From 7df133d35f5773672bb11907218597efde9fcb82 Mon Sep 17 00:00:00 2001 From: Simon Heather <32168619+X-Guardian@users.noreply.github.com> Date: Sat, 2 Nov 2019 15:45:04 +0000 Subject: [PATCH] BREAKING CHANGE: ADManagedServiceAccount: Add KerberosEncryptionType Property and Refactor (#517) - Changes to ADManagedServiceAccount - KerberosEncryptionType property added (issue #511). - BREAKING CHANGE: AccountType parameter ValidateSet changed from ('Group', 'Single') to ('Group', 'Standalone') - Standalone is the correct terminology (issue #515). - BREAKING CHANGE: AccountType parameter default of Single removed. - Enforce positive choice of account type. - BREAKING CHANGE: MembershipAttribute parameter ValidateSet member SID changed to ObjectSid to match result property of Get-AdObject. Previous code does not work if SID is specified. - BREAKING CHANGE: AccountTypeForce parameter removed - unnecessary complication. - BREAKING CHANGE: Members parameter renamed to ManagedPasswordPrincipals - to closer match Get-AdServiceAccount result property PrincipalsAllowedToRetrieveManagedPassword. This is so that a DelegateToAccountPrincipals parameter can be added later. - Common Compare-ResourcePropertyState function used to replace function specific Compare-TargetResourceState and code refactored (issue #512). - Resource unit tests refactored to use nested contexts and follow the logic of the module. - Resource Integration tests added. --- CHANGELOG.md | 13 +- .../MSFT_ADManagedServiceAccount.psm1 | 1077 ++++----- .../MSFT_ADManagedServiceAccount.schema.mof | 16 +- .../MSFT_ADManagedServiceAccount.strings.psd1 | 32 +- .../about_ADManagedServiceAccount.help.txt | 57 +- ...unt_CreateManagedServiceAccount_Config.ps1 | 6 +- ...reateGroupManagedServiceAccount_Config.ps1 | 4 +- ...anagedServiceAccountWithMembers_Config.ps1 | 21 +- ...ManagedServiceAccountCustomPath_Config.ps1 | 41 + ...anagedServiceAccount.Integration.Tests.ps1 | 370 +++ .../MSFT_ADManagedServiceAccount.config.ps1 | 218 ++ .../MSFT_ADManagedServiceAccount.Tests.ps1 | 2069 ++++++----------- 12 files changed, 1854 insertions(+), 2070 deletions(-) create mode 100644 Examples/Resources/ADManagedServiceAccount/4-ADManagedServiceAccount_CreateGroupManagedServiceAccountCustomPath_Config.ps1 create mode 100644 Tests/Integration/MSFT_ADManagedServiceAccount.Integration.Tests.ps1 create mode 100644 Tests/Integration/MSFT_ADManagedServiceAccount.config.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index d64ce0ea1..3eaf478aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,23 @@ - Changes to ADServicePrincipalName - Added Integration testing ([issue #358](https://github.com/PowerShell/ActiveDirectoryDsc/issues/358)). +- Changes to ADManagedServiceAccount + - KerberosEncryptionType property added. ([issue #511](https://github.com/PowerShell/ActiveDirectoryDsc/issues/511)). + - BREAKING CHANGE: AccountType parameter ValidateSet changed from ('Group', 'Single') to ('Group', 'Standalone') - Standalone is the correct terminology. Ref: [Service Accounts](https://docs.microsoft.com/en-us/windows/security/identity-protection/access-control/service-accounts). + ([issue #515](https://github.com/PowerShell/ActiveDirectoryDsc/issues/515)). + - BREAKING CHANGE: AccountType parameter default of Single removed. - Enforce positive choice of account type. + - BREAKING CHANGE: MembershipAttribute parameter ValidateSet member SID changed to ObjectSid to match result property of Get-AdObject. Previous code does not work if SID is specified. + - BREAKING CHANGE: AccountTypeForce parameter removed - unnecessary complication. + - BREAKING CHANGE: Members parameter renamed to ManagedPasswordPrincipals - to closer match Get-AdServiceAccount result property PrincipalsAllowedToRetrieveManagedPassword. This is so that a DelegateToAccountPrincipals parameter can be added later. + - Common Compare-ResourcePropertyState function used to replace function specific Compare-TargetResourceState and code refactored. + ([issue #512](https://github.com/PowerShell/ActiveDirectoryDsc/issues/512)). + - Resource unit tests refactored to use nested contexts and follow the logic of the module. + - Resource Integration tests added. ## 4.2.0.0 - Changes to ActiveDirectoryDsc - Resolved custom Script Analyzer rules that was added to the test framework. - - Resolve style guideline violations for hashtables ([issue #516](https://github.com/PowerShell/ActiveDirectoryDsc/issues/516)). - Changes to ADReplicationSite - Added 'Description' attribute parameter ([issue #500](https://github.com/PowerShell/ActiveDirectoryDsc/issues/500)). - Added Integration testing ([issue #355](https://github.com/PowerShell/ActiveDirectoryDsc/issues/355)). diff --git a/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.psm1 b/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.psm1 index 99f843f26..f6edc3e7b 100644 --- a/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.psm1 +++ b/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.psm1 @@ -6,30 +6,43 @@ Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath ' $script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADManagedServiceAccount' +$script:errorCodeKdsRootKeyNotFound = -2146893811 + <# .SYNOPSIS - Gets the specified managed service account. + Returns the current state of an Active Directory managed service account. .PARAMETER ServiceAccountName - Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName'). - To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created, - the user's SamAccountName and CN cannot be changed. - - .PARAMETER MembershipAttribute - Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs). - If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType' + Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName + 'sAMAccountName'). To be compatible with older operating systems, create a SAM account name that is 20 characters + or less. Once created, the user's SamAccountName and CN cannot be changed. - .PARAMETER AccountTypeForce - Specifies whether or not to remove the service account and recreate it when going from single MSA to - group MSA and vice-versa. If not specified, this value defaults to False. + .PARAMETER AccountType + The type of managed service account. Standalone will create a Standalone Managed Service Account (sMSA) and + Group will create a Group Managed Service Account (gMSA). .PARAMETER Credential Specifies the user account credentials to use to perform this task. - This is only required if not executing the task on a domain controller or using the -DomainController parameter. + This is only required if not executing the task on a domain controller or using the DomainController parameter. .PARAMETER DomainController Specifies the Active Directory Domain Controller instance to use to perform the task. This is only required if not executing the task on a domain controller. + + .PARAMETER MembershipAttribute + Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs). + If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Get-ADObject | ActiveDirectory + Get-ADServiceAccount | ActiveDirectory + Assert-Module | ActiveDirectoryDsc.Common + Get-ADCommonParameters | ActiveDirectoryDsc.Common + Get-ADObjectParentDN | ActiveDirectoryDsc.Common + New-InvalidOperationException | ActiveDirectoryDsc.Common #> function Get-TargetResource { @@ -42,15 +55,10 @@ function Get-TargetResource [System.String] $ServiceAccountName, - [Parameter()] - [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')] + [Parameter(Mandatory = $true)] + [ValidateSet('Group', 'Standalone')] [System.String] - $MembershipAttribute = 'SamAccountName', - - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.Boolean] - $AccountTypeForce = $false, + $AccountType, [Parameter()] [ValidateNotNull()] @@ -61,132 +69,173 @@ function Get-TargetResource [Parameter()] [ValidateNotNullOrEmpty()] [System.String] - $DomainController + $DomainController, + + [Parameter()] + [ValidateSet('SamAccountName', 'DistinguishedName', 'ObjectSid', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName' ) Assert-Module -ModuleName 'ActiveDirectory' $adServiceAccountParameters = Get-ADCommonParameters @PSBoundParameters - $targetResource = @{ - ServiceAccountName = $ServiceAccountName - DistinguishedName = $null - Path = $null - Description = $null - DisplayName = $null - AccountType = $null - AccountTypeForce = $AccountTypeForce - Ensure = $null - Enabled = $false - Members = @() - MembershipAttribute = $MembershipAttribute - Credential = $Credential - DomainController = $DomainController - } + Write-Verbose -Message ($script:localizedData.RetrievingManagedServiceAccountMessage -f + $ServiceAccountName) try { - Write-Verbose -Message ($script:localizedData.RetrievingServiceAccount -f $ServiceAccountName) - $adServiceAccount = Get-ADServiceAccount @adServiceAccountParameters -Properties @( - 'Name' 'DistinguishedName' 'Description' 'DisplayName' 'ObjectClass' 'Enabled' 'PrincipalsAllowedToRetrieveManagedPassword' - 'SamAccountName' - 'DistinguishedName' - 'SID' - 'ObjectGUID' + 'KerberosEncryptionType' ) + } - $targetResource['Ensure'] = 'Present' - $targetResource['Path'] = Get-ADObjectParentDN -DN $adServiceAccount.DistinguishedName - $targetResource['Description'] = $adServiceAccount.Description - $targetResource['DisplayName'] = $adServiceAccount.DisplayName - $targetResource['Enabled'] = [System.Boolean] $adServiceAccount.Enabled - $targetResource['DistinguishedName'] = $adServiceAccount.DistinguishedName + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + Write-Verbose -Message ($script:localizedData.ManagedServiceAccountNotFoundMessage -f + $AccountType, $ServiceAccountName) + } + catch + { + $errorMessage = $script:localizedData.RetrievingManagedServiceAccountError -f $ServiceAccountName + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } - if ( $adServiceAccount.ObjectClass -eq 'msDS-ManagedServiceAccount' ) + if ($adServiceAccount) + { + # Resource exists + if ($adServiceAccount.ObjectClass -eq 'msDS-ManagedServiceAccount') { - $targetResource['AccountType'] = 'Single' + $existingAccountType = 'Standalone' } - elseif ( $adServiceAccount.ObjectClass -eq 'msDS-GroupManagedServiceAccount' ) + else { - Write-Verbose -Message ($script:localizedData.RetrievingPrincipalMembers -f $MembershipAttribute) - $adServiceAccount.PrincipalsAllowedToRetrieveManagedPassword | - ForEach-Object { - $member = (Get-ADObject -Identity $_ -Properties $MembershipAttribute).$MembershipAttribute - $targetResource['Members'] += $member + $existingAccountType = 'Group' + + Write-Verbose -Message ($script:localizedData.RetrievingManagedPasswordPrincipalsMessage -f + $MembershipAttribute) + + $managedPasswordPrincipals = @() + + foreach ($identity in $adServiceAccount.PrincipalsAllowedToRetrieveManagedPassword) + { + try + { + $principal = (Get-ADObject -Identity $identity -Properties $MembershipAttribute).$MembershipAttribute + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + # Add unresolved SID as principal if the identity could not be found + $principal = $identity + } + catch + { + $errorMessage = $script:localizedData.RetrievingManagedPasswordPrincipalsError -f $identity + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ } - $targetResource['AccountType'] = 'Group' + $managedPasswordPrincipals += $principal + } + } + + $targetResource = @{ + ServiceAccountName = $ServiceAccountName + AccountType = $existingAccountType + Path = Get-ADObjectParentDN -DN $adServiceAccount.DistinguishedName + Description = $adServiceAccount.Description + DisplayName = $adServiceAccount.DisplayName + DistinguishedName = $adServiceAccount.DistinguishedName + Enabled = $adServiceAccount.Enabled + KerberosEncryptionType = $adServiceAccount.KerberosEncryptionType -split (', ') + ManagedPasswordPrincipals = $managedPasswordPrincipals + MembershipAttribute = $MembershipAttribute + Ensure = 'Present' } } - catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] - { - Write-Verbose -Message ($script:localizedData.ManagedServiceAccountNotFound -f $ServiceAccountName) - $targetResource['Ensure'] = 'Absent' - } - catch + else { - $errorMessage = $script:localizedData.RetrievingServiceAccountError -f $ServiceAccountName - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + # Resource does not exist + $targetResource = @{ + ServiceAccountName = $ServiceAccountName + AccountType = $AccountType + Path = $null + Description = $null + DisplayName = $null + DistinguishedName = $null + Enabled = $false + KerberosEncryptionType = @() + ManagedPasswordPrincipals = @() + MembershipAttribute = $MembershipAttribute + Ensure = 'Absent' + } } return $targetResource } #end function Get-TargetResource - <# .SYNOPSIS - Tests the state of the managed service account. + Tests if an Active Directory managed service account is in the desired state. .PARAMETER ServiceAccountName - Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName'). - To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created, - the user's SamAccountName and CN cannot be changed. + Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName + 'sAMAccountName'). To be compatible with older operating systems, create a SAM account name that is 20 + characters or less. Once created, the user's SamAccountName and CN cannot be changed. .PARAMETER AccountType - The type of managed service account. Single will create a Single Managed Service Account (sMSA) and Group will - create a Group Managed Service Account (gMSA). If not specified, this vaule defaults to Single. + The type of managed service account. Standalone will create a Standalone Managed Service Account (sMSA) and + Group will create a Group Managed Service Account (gMSA). - .PARAMETER AccountTypeForce - Specifies whether or not to remove the service account and recreate it when going from single MSA to - group MSA and vice-versa. If not specified, this value defaults to False. + .PARAMETER Credential + Specifies the user account credentials to use to perform this task. + This is only required if not executing the task on a domain controller or using the DomainController parameter. - .PARAMETER Path - Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. - Specified as a Distinguished Name (DN). + .PARAMETER Description + Specifies the description of the account (ldapDisplayName 'description'). + + .PARAMETER DisplayName + Specifies the display name of the account (ldapDisplayName 'displayName'). + + .PARAMETER DomainController + Specifies the Active Directory Domain Controller instance to use to perform the task. + This is only required if not executing the task on a domain controller. .PARAMETER Ensure Specifies whether the user account is created or deleted. If not specified, this value defaults to Present. - .PARAMETER Description - Specifies a description of the object (ldapDisplayName 'description'). + .PARAMETER KerberosEncryptionType + Specifies which Kerberos encryption types the account supports when creating service tickets. + This value sets the encryption types supported flags of the Active Directory msDS-SupportedEncryptionTypes + attribute. - .PARAMETER DisplayName - Specifies the display name of the object (ldapDisplayName 'displayName'). - - .PARAMETER Members - Specifies the members of the object (ldapDisplayName 'PrincipalsAllowedToRetrieveManagedPassword'). - Only used when 'Group' is selected for 'AccountType'. + .PARAMETER ManagedPasswordPrincipals + Specifies the membership policy for systems which can use a group managed service account. (ldapDisplayName + 'msDS-GroupMSAMembership'). Only used when 'Group' is selected for 'AccountType'. .PARAMETER MembershipAttribute Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs). If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'. - .PARAMETER Credential - Specifies the user account credentials to use to perform this task. - This is only required if not executing the task on a domain controller or using the -DomainController parameter. + .PARAMETER Path + Specifies the X.500 path of the Organizational Unit (OU) or container where the new account is created. + Specified as a Distinguished Name (DN). - .PARAMETER DomainController - Specifies the Active Directory Domain Controller instance to use to perform the task. - This is only required if not executing the task on a domain controller. + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Compare-ResourcePropertyState | ActiveDirectoryDsc.Common #> function Test-TargetResource { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', "", + Justification = 'False positive on ManagedPasswordPrincipals')] [CmdletBinding()] [OutputType([System.Boolean])] param @@ -196,149 +245,193 @@ function Test-TargetResource [System.String] $ServiceAccountName, - [Parameter()] - [ValidateSet('Group', 'Single')] + [Parameter(Mandatory = $true)] + [ValidateSet('Group', 'Standalone')] [System.String] - $AccountType = 'Single', + $AccountType, [Parameter()] - [ValidateNotNullOrEmpty()] - [System.Boolean] - $AccountTypeForce = $false, + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, [Parameter()] [System.String] - $Path, + $Description, [Parameter()] - [ValidateSet('Present', 'Absent')] [System.String] - $Ensure = 'Present', + $DisplayName, [Parameter()] + [ValidateNotNullOrEmpty()] [System.String] - $Description, + $DomainController, [Parameter()] + [ValidateSet('Present', 'Absent')] [System.String] - $DisplayName, + $Ensure = 'Present', [Parameter()] + [ValidateSet('None', 'RC4', 'AES128', 'AES256')] [System.String[]] - $Members, + $KerberosEncryptionType, [Parameter()] - [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')] - [System.String] - $MembershipAttribute = 'SamAccountName', + [System.String[]] + $ManagedPasswordPrincipals, [Parameter()] - [ValidateNotNull()] - [System.Management.Automation.PSCredential] - [System.Management.Automation.CredentialAttribute()] - $Credential, + [ValidateSet('SamAccountName', 'DistinguishedName', 'ObjectSid', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName', [Parameter()] - [ValidateNotNullOrEmpty()] [System.String] - $DomainController + $Path ) # Need to set these parameters to compare if users are using the default parameter values - $PSBoundParameters['Ensure'] = $Ensure - $PSBoundParameters['AccountType'] = $AccountType - $PSBoundParameters['MembershipAttribute'] = $MembershipAttribute + [HashTable] $parameters = $PSBoundParameters + $parameters['MembershipAttribute'] = $MembershipAttribute + + $getTargetResourceParameters = @{ + ServiceAccountName = $ServiceAccountName + AccountType = $AccountType + DomainController = $DomainController + MembershipAttribute = $MembershipAttribute + } - $compareTargetResourceNonCompliant = Compare-TargetResourceState @PSBoundParameters | - Where-Object -FilterScript { $_.Pass -eq $false } + @($getTargetResourceParameters.Keys) | + ForEach-Object { + if (-not $parameters.ContainsKey($_)) + { + $getTargetResourceParameters.Remove($_) + } + } - # Check if Absent, if so then we don't need to propagate any other parameters - if ($Ensure -eq 'Absent') - { - $ensureState = $compareTargetResourceNonCompliant | - Where-Object -FilterScript { $_.Parameter -eq 'Ensure' } + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters - if ($ensureState) + if ($getTargetResourceResult.Ensure -eq 'Present') + { + # Resource exists + if ($Ensure -eq 'Present') { - Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f ` - 'Ensure', $ensureState.Expected, $ensureState.Actual) + # Resource should exist + $propertiesNotInDesiredState = ( + Compare-ResourcePropertyState -CurrentValues $getTargetResourceResult -DesiredValues $parameters ` + -IgnoreProperties 'DomainController', 'Credential' | Where-Object -Property InDesiredState -eq $false) + + if ($propertiesNotInDesiredState) + { + $inDesiredState = $false + } + else + { + # Resource is in desired state + Write-Verbose -Message ($script:localizedData.ManagedServiceAccountInDesiredStateMessage -f + $AccountType, $ServiceAccountName) + $inDesiredState = $true + } } else { - Write-Verbose -Message ($script:localizedData.MSAInDesiredState -f $ServiceAccountName) - return $true + # Resource should not exist + Write-Verbose -Message ($script:localizedData.ResourceExistsButShouldNotMessage -f + $AccountType, $ServiceAccountName) + $inDesiredState = $false } } else { - $compareTargetResourceNonCompliant | - ForEach-Object { - Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f ` - $_.Parameter, $_.Expected, $_.Actual) - } - } - - if ($compareTargetResourceNonCompliant) - { - Write-Verbose -Message ($script:localizedData.MSANotInDesiredState -f $ServiceAccountName) - return $false - } - else - { - Write-Verbose -Message ($script:localizedData.MSAInDesiredState -f $ServiceAccountName) - return $true + # Resource does not exist + if ($Ensure -eq 'Present') + { + # Resource should exist + Write-Verbose -Message ($script:localizedData.ResourceDoesNotExistButShouldMessage -f + $AccountType, $ServiceAccountName) + $inDesiredState = $false + } + else + { + # Resource should not exist + Write-Verbose -Message ($script:localizedData.ManagedServiceAccountInDesiredStateMessage -f + $AccountType, $ServiceAccountName) + $inDesiredState = $true + } } + $inDesiredState } #end function Test-TargetResource <# .SYNOPSIS - Sets the state of the managed service account. + Sets the state of an Active Directory managed service account. .PARAMETER ServiceAccountName - Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName'). - To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created, - the user's SamAccountName and CN cannot be changed. + Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName + 'sAMAccountName'). To be compatible with older operating systems, create a SAM account name that is 20 + characters or less. Once created, the user's SamAccountName and CN cannot be changed. .PARAMETER AccountType - The type of managed service account. Single will create a Single Managed Service Account (sMSA) and Group will - create a Group Managed Service Account (gMSA). If not specified, this vaule defaults to Single. + The type of managed service account. Standalone will create a Standalone Managed Service Account (sMSA) and + Group will create a Group Managed Service Account (gMSA). - .PARAMETER AccountTypeForce - Specifies whether or not to remove the service account and recreate it when going from single MSA to - group MSA and vice-versa. If not specified, this value defaults to False. + .PARAMETER Credential + Specifies the user account credentials to use to perform this task. + This is only required if not executing the task on a domain controller or using the DomainController parameter. - .PARAMETER Path - Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. - Specified as a Distinguished Name (DN). + .PARAMETER Description + Specifies the description of the account (ldapDisplayName 'description'). + + .PARAMETER DisplayName + Specifies the display name of the account (ldapDisplayName 'displayName'). + + .PARAMETER DomainController + Specifies the Active Directory Domain Controller instance to use to perform the task. + This is only required if not executing the task on a domain controller. .PARAMETER Ensure Specifies whether the user account is created or deleted. If not specified, this value defaults to Present. - .PARAMETER Description - Specifies a description of the object (ldapDisplayName 'description'). - - .PARAMETER DisplayName - Specifies the display name of the object (ldapDisplayName 'displayName'). + .PARAMETER KerberosEncryptionType + Specifies which Kerberos encryption types the account supports when creating service tickets. + This value sets the encryption types supported flags of the Active Directory msDS-SupportedEncryptionTypes + attribute. - .PARAMETER Members - Specifies the members of the object (ldapDisplayName 'PrincipalsAllowedToRetrieveManagedPassword'). - Only used when 'Group' is selected for 'AccountType'. + .PARAMETER ManagedPasswordPrincipals + Specifies the membership policy for systems which can use a group managed service account. (ldapDisplayName + 'msDS-GroupMSAMembership'). Only used when 'Group' is selected for 'AccountType'. .PARAMETER MembershipAttribute Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs). If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'. - .PARAMETER Credential - Specifies the user account credentials to use to perform this task. - This is only required if not executing the task on a domain controller or using the -DomainController parameter. + .PARAMETER Path + Specifies the X.500 path of the Organizational Unit (OU) or container where the new account is created. + Specified as a Distinguished Name (DN). - .PARAMETER DomainController - Specifies the Active Directory Domain Controller instance to use to perform the task. - This is only required if not executing the task on a domain controller. + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Get-ADDomain | ActiveDirectory + Move-ADObject | ActiveDirectory + New-ADServiceAccount | ActiveDirectory + Remove-ADServiceAccount | ActiveDirectory + Set-ADServiceAccount | ActiveDirectory + Compare-ResourcePropertyState | ActiveDirectoryDsc.Common + Get-ADCommonParameters | ActiveDirectoryDsc.Common + Get-DomainName | ActiveDirectoryDsc.Common + New-InvalidOperationException | ActiveDirectoryDsc.Common #> + function Set-TargetResource { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', "", + Justification = 'False positive on ManagedPasswordPrincipals')] [CmdletBinding()] param ( @@ -347,531 +440,293 @@ function Set-TargetResource [System.String] $ServiceAccountName, - [Parameter()] - [ValidateSet('Group', 'Single')] + [Parameter(Mandatory = $true)] + [ValidateSet('Group', 'Standalone')] [System.String] - $AccountType = 'Single', + $AccountType, [Parameter()] - [ValidateNotNullOrEmpty()] - [System.Boolean] - $AccountTypeForce = $false, + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, [Parameter()] - [ValidateNotNullOrEmpty()] [System.String] - $Path, + $Description, [Parameter()] - [ValidateSet('Present', 'Absent')] [System.String] - $Ensure = 'Present', + $DisplayName, [Parameter()] + [ValidateNotNullOrEmpty()] [System.String] - $Description, + $DomainController, [Parameter()] + [ValidateSet('Present', 'Absent')] [System.String] - $DisplayName, + $Ensure = 'Present', [Parameter()] + [ValidateSet('None', 'RC4', 'AES128', 'AES256')] [System.String[]] - $Members, + $KerberosEncryptionType, [Parameter()] - [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')] - [System.String] - $MembershipAttribute = 'SamAccountName', + [System.String[]] + $ManagedPasswordPrincipals, [Parameter()] - [ValidateNotNull()] - [System.Management.Automation.PSCredential] - [System.Management.Automation.CredentialAttribute()] - $Credential, + [ValidateSet('SamAccountName', 'DistinguishedName', 'ObjectSid', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName', [Parameter()] [ValidateNotNullOrEmpty()] [System.String] - $DomainController + $Path ) # Need to set these to compare if not specified since user is using defaults - $PSBoundParameters['Ensure'] = $Ensure - $PSBoundParameters['AccountType'] = $AccountType - $PSBoundParameters['MembershipAttribute'] = $MembershipAttribute + [HashTable] $parameters = $PSBoundParameters + $parameters['MembershipAttribute'] = $MembershipAttribute - $compareTargetResource = Compare-TargetResourceState @PSBoundParameters - $compareTargetResourceNonCompliant = @($compareTargetResource | - Where-Object -FilterScript { $_.Pass -eq $false }) + $adServiceAccountParameters = Get-ADCommonParameters @parameters - $adServiceAccountParameters = Get-ADCommonParameters @PSBoundParameters - $setServiceAccountParameters = $adServiceAccountParameters.Clone() - $moveADObjectParameters = $adServiceAccountParameters.Clone() - - try - { - if ($Ensure -eq 'Present') - { - $isEnsureNonCompliant = $false + $getTargetResourceParameters = @{ + ServiceAccountName = $ServiceAccountName + AccountType = $AccountType + DomainController = $DomainController + MembershipAttribute = $MembershipAttribute + } - if ($compareTargetResourceNonCompliant | - Where-Object -FilterScript { $_.Parameter -eq 'Ensure' }) + @($getTargetResourceParameters.Keys) | + ForEach-Object { + if (-not $parameters.ContainsKey($_)) { - $isEnsureNonCompliant = $true + $getTargetResourceParameters.Remove($_) } + } - # We want the account to be present, but it currently does not exist - if ($isEnsureNonCompliant) - { - $null = $PSBoundParameters.Remove('AccountTypeForce') - New-ADServiceAccountHelper @PSBoundParameters - } - else - { - #region Check if AccountType is compliant - $accountTypeState = $compareTargetResourceNonCompliant | - Where-Object -FilterScript { $_.Parameter -eq 'AccountType' } + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters - # Account already exist, need to update parameters that are not in compliance - if ($accountTypeState) + if ($Ensure -eq 'Present') + { + # Resource should be present + if ($getTargetResourceResult.Ensure -eq 'Present') + { + # Resource is present + $createNewAdServiceAccount = $false + $propertiesNotInDesiredState = ( + Compare-ResourcePropertyState -CurrentValues $getTargetResourceResult -DesiredValues $parameters ` + -IgnoreProperties 'DomainController', 'Credential' | Where-Object -Property InDesiredState -eq $false) + if ($propertiesNotInDesiredState) + { + if ($propertiesNotInDesiredState.ParameterName -contains 'AccountType') { - if ($AccountTypeForce) + # AccountType has changed, so the account needs recreating + Write-Verbose -Message ($script:localizedData.RecreatingManagedServiceAccountMessage -f + $AccountType, $ServiceAccountName) + try { - # We need to recreate account first before we can update any properties - Write-Verbose -Message ($script:localizedData.UpdatingManagedServiceAccountProperty -f 'AccountType', $AccountType) Remove-ADServiceAccount @adServiceAccountParameters -Confirm:$false - $null = $PSBoundParameters.Remove('AccountTypeForce') - New-ADServiceAccountHelper @PSBoundParameters } - else + catch { - Write-Warning -Message ($script:localizedData.AccountTypeForceNotTrue -f $accountTypeState.Actual, $accountTypeState.Expected) + $errorMessage = ($script:localizedData.RemovingManagedServiceAccountError -f + $AccountType, $ServiceAccountName) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ } - } - # Remove AccountType since we don't want to enumerate down below - $compareTargetResourceNonCompliant = @($compareTargetResourceNonCompliant | - Where-Object -FilterScript { $_.Parameter -ne 'AccountType' }) - #endregion Check if AccountType is compliant - - #region Check if Path is compliant - $isPathNonCompliant = $false - if ($compareTargetResourceNonCompliant | - Where-Object -FilterScript { $_.Parameter -eq 'Path' }) - { - $isPathNonCompliant = $true + $createNewAdServiceAccount = $true } - - if ($isPathNonCompliant) + else { - Write-Verbose -Message ($script:localizedData.MovingManagedServiceAccount -f $ServiceAccountName, $Path) - $distinguishedNameObject = $compareTargetResource | - Where-Object -FilterScript { $_.Parameter -eq 'DistinguishedName' } + $setServiceAccountParameters = $adServiceAccountParameters.Clone() + $setAdServiceAccountRequired = $false + $moveAdServiceAccountRequired = $false - $moveADObjectParameters['Identity'] = $distinguishedNameObject.Actual + foreach ($property in $propertiesNotInDesiredState) + { + if ($property.ParameterName -eq 'Path') + { + # The path has changed, so the account needs moving, but not until after any other changes + $moveAdServiceAccountRequired = $true + } + else + { + $setAdServiceAccountRequired = $true - Move-ADObject @moveADObjectParameters -TargetPath $Path - } + Write-Verbose -Message ($script:localizedData.UpdatingManagedServiceAccountPropertyMessage -f + $AccountType, $ServiceAccountName, $property.ParameterName, ($property.Expected -join ', ')) - $compareTargetResourceNonCompliant = @($compareTargetResourceNonCompliant | - Where-Object -FilterScript { $_.Parameter -ne 'Path' }) - #endregion Check if Path is compliant - - #region Check if other parameters are compliant - $updateProperties = $false - $compareTargetResourceNonCompliant | - ForEach-Object { - $updateProperties = $true - $parameter = $_.Parameter - if ($parameter -eq 'Members' -and $AccountType -eq 'Group') - { - if ([system.string]::IsNullOrEmpty($Members)) + if ($property.ParameterName -eq 'ManagedPasswordPrincipals' -and $AccountType -eq 'Group') { - $Members = @() + $setServiceAccountParameters.Add('PrincipalsAllowedToRetrieveManagedPassword', + $ManagedPasswordPrincipals) } - $listMembers = $Members -join ',' + else + { + $SetServiceAccountParameters.Add($property.ParameterName, $property.Expected) + } + } + } - Write-Verbose -Message ($script:localizedData.UpdatingManagedServiceAccountProperty -f 'Members', $listMembers) - $setServiceAccountParameters['PrincipalsAllowedToRetrieveManagedPassword'] = $Members + if ($setAdServiceAccountRequired) + { + try + { + Set-ADServiceAccount @setServiceAccountParameters } - else + catch { - Write-Verbose -Message ($script:localizedData.UpdatingManagedServiceAccountProperty -f $parameter, $PSBoundParameters.$parameter) - $setServiceAccountParameters[$parameter] = $PSBoundParameters.$parameter + $errorMessage = ($script:localizedData.SettingManagedServiceAccountError -f + $AccountType, $ServiceAccountName) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ } } - if ($compareTargetResourceNonCompliant.Count -gt 0) - { - Set-ADServiceAccount @setServiceAccountParameters + if ($moveAdServiceAccountRequired) + { + Write-Verbose -Message ($script:localizedData.MovingManagedServiceAccountMessage -f + $AccountType, $ServiceAccountName, $getTargetResourceResult.Path, $Path) + $moveADObjectParameters = $adServiceAccountParameters.Clone() + $moveADObjectParameters.Identity = $getTargetResourceResult.DistinguishedName + try + { + Move-ADObject @moveADObjectParameters -TargetPath $Path + } + catch + { + $errorMessage = ($script:localizedData.MovingManagedServiceAccountError -f + $AccountType, $ServiceAccountName, $getTargetResourceResult.Path, $Path) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } } - #endregion Check if other parameters are compliant } } - elseif ($Ensure -eq 'Absent') + else { - $isEnsureNonCompliant = $false - if ($compareTargetResourceNonCompliant | - Where-Object -FilterScript { $_.Parameter -eq 'Ensure' }) - { - $isEnsureNonCompliant = $true - } - - # We want the account to be Absent, but it is Present - if ($isEnsureNonCompliant) - { - Write-Verbose -Message ($script:localizedData.RemovingManagedServiceAccount -f $ServiceAccountName) - Remove-ADServiceAccount @adServiceAccountParameters -Confirm:$false - } + # Resource is absent + $createNewAdServiceAccount = $true } - } - catch - { - $errorMessage = $script:localizedData.AddingManagedServiceAccountError -f $ServiceAccountName - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ - } -} #end function Set-TargetResource - -<# - .SYNOPSIS - Adds the managed service account. - - .PARAMETER ServiceAccountName - Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName'). - To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created, - the user's SamAccountName and CN cannot be changed. - - .PARAMETER AccountType - The type of managed service account. Single will create a Single Managed Service Account (sMSA) and Group will - create a Group Managed Service Account (gMSA). If not specified, this vaule defaults to Single. - - .PARAMETER AccountTypeForce - Specifies whether or not to remove the service account and recreate it when going from single MSA to - group MSA and vice-versa. If not specified, this value defaults to False. - - .PARAMETER Path - Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. - Specified as a Distinguished Name (DN). - - .PARAMETER Ensure - Specifies whether the user account is created or deleted. If not specified, this value defaults to Present. - - .PARAMETER Description - Specifies a description of the object (ldapDisplayName 'description'). - - .PARAMETER DisplayName - Specifies the display name of the object (ldapDisplayName 'displayName'). - - .PARAMETER Members - Specifies the members of the object (ldapDisplayName 'PrincipalsAllowedToRetrieveManagedPassword'). - Only used when 'Group' is selected for 'AccountType'. - - .PARAMETER MembershipAttribute - Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs). - If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'. - - .PARAMETER Credential - Specifies the user account credentials to use to perform this task. - This is only required if not executing the task on a domain controller or using the -DomainController parameter. - - .PARAMETER DomainController - Specifies the Active Directory Domain Controller instance to use to perform the task. - This is only required if not executing the task on a domain controller. -#> -function New-ADServiceAccountHelper -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $ServiceAccountName, - - [Parameter()] - [ValidateSet('Group', 'Single')] - [System.String] - $AccountType = 'Single', - - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.String] - $Path, - - [Parameter()] - [ValidateSet('Present', 'Absent')] - [System.String] - $Ensure = 'Present', - [Parameter()] - [System.String] - $Description, - - [Parameter()] - [System.String] - $DisplayName, - - [Parameter()] - [System.String[]] - $Members, - - [Parameter()] - [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')] - [System.String] - $MembershipAttribute = 'SamAccountName', - - [Parameter()] - [ValidateNotNull()] - [System.Management.Automation.PSCredential] - [System.Management.Automation.CredentialAttribute()] - $Credential, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.String] - $DomainController - ) - - Write-Verbose -Message ($script:localizedData.AddingManagedServiceAccount -f $ServiceAccountName) - - $adServiceAccountParameters = Get-ADCommonParameters @PSBoundParameters -UseNameParameter - - if ($Description) - { - $adServiceAccountParameters['Description'] = $Description - } - - if ($DisplayName) - { - $adServiceAccountParameters['DisplayName'] = $DisplayName - } - - if ($Path) - { - $adServiceAccountParameters['Path'] = $Path - } - - - # Create service account - if ( $AccountType -eq 'Single' ) - { - New-ADServiceAccount @adServiceAccountParameters -RestrictToSingleComputer -PassThru - } - elseif ( $AccountType -eq 'Group' ) - { - if ($Members) + if ($createNewAdServiceAccount) { - $adServiceAccountParameters['PrincipalsAllowedToRetrieveManagedPassword'] = $Members - } - - $dnsHostName = '{0}.{1}' -f $ServiceAccountName, $(Get-DomainName) - $adServiceAccountParameters['DNSHostName'] = $dnsHostName - - New-ADServiceAccount @adServiceAccountParameters -PassThru - } -} #end function New-ADServiceAccountHelper - -<# - .SYNOPSIS - Compares the state of the managed service account. - - .PARAMETER ServiceAccountName - Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName'). - To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created, - the user's SamAccountName and CN cannot be changed. - - .PARAMETER AccountType - The type of managed service account. Single will create a Single Managed Service Account (sMSA) and Group will - create a Group Managed Service Account (gMSA). If not specified, this vaule defaults to Single. - - .PARAMETER AccountTypeForce - Specifies whether or not to remove the service account and recreate it when going from single MSA to - group MSA and vice-versa. If not specified, this value defaults to False. - - .PARAMETER Path - Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. - Specified as a Distinguished Name (DN). - - .PARAMETER Ensure - Specifies whether the user account is created or deleted. If not specified, this value defaults to Present. - - .PARAMETER Description - Specifies a description of the object (ldapDisplayName 'description'). - - .PARAMETER DisplayName - Specifies the display name of the object (ldapDisplayName 'displayName'). - - .PARAMETER Members - Specifies the members of the object (ldapDisplayName 'PrincipalsAllowedToRetrieveManagedPassword'). - Only used when 'Group' is selected for 'AccountType'. - - .PARAMETER MembershipAttribute - Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs). - If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'. - - .PARAMETER Credential - Specifies the user account credentials to use to perform this task. - This is only required if not executing the task on a domain controller or using the -DomainController parameter. - - .PARAMETER DomainController - Specifies the Active Directory Domain Controller instance to use to perform the task. - This is only required if not executing the task on a domain controller. -#> -function Compare-TargetResourceState -{ - [CmdletBinding()] - [OutputType([System.Array])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $ServiceAccountName, - - [Parameter()] - [ValidateSet('Group', 'Single')] - [System.String] - $AccountType, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.Boolean] - $AccountTypeForce, - - [Parameter()] - [System.String] - $Path, - - [Parameter()] - [ValidateSet('Present', 'Absent')] - [System.String] - $Ensure, - - [Parameter()] - [System.String] - $Description, - - [Parameter()] - [System.String] - $DisplayName, - - [Parameter()] - [System.String[]] - $Members, - - [Parameter()] - [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')] - [System.String] - $MembershipAttribute, + if (-not $parameters.ContainsKey('Path')) + { + # Get default MSA path as one has not been specified + try + { + $domainDistinguishedName = (Get-ADDomain).DistinguishedName + } + catch + { + $errorMessage = $script:localizedData.GettingADDomainError + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } - [Parameter()] - [ValidateNotNull()] - [System.Management.Automation.PSCredential] - [System.Management.Automation.CredentialAttribute()] - $Credential, + $messagePath = "CN=Managed Service Accounts,$domainDistinguishedName" + } + else + { + $messagePath = $Path + } - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.String] - $DomainController - ) + Write-Verbose -Message ($script:localizedData.AddingManagedServiceAccountMessage -f + $AccountType, $ServiceAccountName, $messagePath) - $getTargetResourceParameters = @{ - ServiceAccountName = $ServiceAccountName - Credential = $Credential - DomainController = $DomainController - MembershipAttribute = $MembershipAttribute - AccountTypeForce = $AccountTypeForce - } + $newAdServiceAccountParameters = Get-ADCommonParameters @parameters -UseNameParameter - @($getTargetResourceParameters.Keys) | - ForEach-Object { - if (-not $PSBoundParameters.ContainsKey($_)) + if ($parameters.ContainsKey('Description')) { - $getTargetResourceParameters.Remove($_) + $newAdServiceAccountParameters.Description = $Description } - } - - $getTargetResource = Get-TargetResource @getTargetResourceParameters - $compareTargetResource = @() - # Add DistinguishedName as it won't be passed as an argument, but we want to get the DN in Set - $PSBoundParameters['DistinguishedName'] = $getTargetResource['DistinguishedName'] + if ($parameters.ContainsKey('DisplayName')) + { + $newAdServiceAccountParameters.DisplayName = $DisplayName + } - <# - Set MembershipAttribute as it's not required to be compliant. It's only - used when setting/getting members for gMSA and there is no way to check - if it is in compliance since whatever is passed would be compliant itself. - #> - $PSBoundParameters['MembershipAttribute'] = $getTargetResource['MembershipAttribute'] + if ($parameters.ContainsKey('Path')) + { + $newAdServiceAccountParameters.Path = $Path + } - foreach ($parameter in $PSBoundParameters.Keys) - { - if ($PSBoundParameters.$parameter -eq $getTargetResource.$parameter) - { - # Check if parameter is in compliance - $compareTargetResource += [pscustomobject] @{ - Parameter = $parameter - Expected = $PSBoundParameters.$parameter - Actual = $getTargetResource.$parameter - Pass = $true + if ( $AccountType -eq 'Standalone' ) + { + # Create standalone managed service account + $newAdServiceAccountParameters.RestrictToSingleComputer = $true } - } - elseif ($parameter -eq 'Members') - { - # Members is only for Group MSAs, if it's single computer, we can skip over this parameter - if ($PSBoundParameters.AccountType -eq 'Group') + else { - $testMembersParams = @{ - ExistingMembers = $getTargetResource.Members -as [System.String[]] - Members = $Members - } + # Create group managed service account + $newAdServiceAccountParameters.DNSHostName = "$ServiceAccountName.$(Get-DomainName)" - $expectedMembers = ($Members | - Sort-Object) -join ',' - - $actualMembers = ($testMembersParams['ExistingMembers'] | - Sort-Object) -join ',' + if ($parameters.ContainsKey('ManagedPasswordPrincipals')) + { + $newAdServiceAccountParameters.PrincipalsAllowedToRetrieveManagedPassword = ` + $ManagedPasswordPrincipals + } + } - if (-not (Test-Members @testMembersParams)) + try + { + New-ADServiceAccount @newAdServiceAccountParameters + } + catch [Microsoft.ActiveDirectory.Management.ADException] + { + if ($_.Exception.ErrorCode -eq $script:errorCodeKdsRootKeyNotFound) { - $compareTargetResource += [pscustomobject] @{ - Parameter = $parameter - Expected = $expectedMembers - Actual = $actualMembers - Pass = $false - } + $errorMessage = ($script:localizedData.KdsRootKeyNotFoundError -f + $ServiceAccountName) } else { - $compareTargetResource += [pscustomobject] @{ - Parameter = $parameter - Expected = $expectedMembers - Actual = $actualMembers - Pass = $true - } + $errorMessage = ($script:localizedData.AddingManagedServiceAccountError -f + $AccountType, $ServiceAccountName, $messagePath) } + + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + catch + { + $errorMessage = ($script:localizedData.AddingManagedServiceAccountError -f + $AccountType, $ServiceAccountName, $messagePath) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ } } - - # Need to check if parameter is part of schema, otherwise ignore all other parameters like verbose - elseif ($getTargetResource.ContainsKey($parameter)) + } + else + { + # Resource should be absent + if ($getTargetResourceResult.Ensure -eq 'Present') { - <# - We are out of compliance if we get here - $PSBoundParameters.$parameter -ne $getTargetResource.$parameter - #> - $compareTargetResource += [pscustomobject] @{ - Parameter = $parameter - Expected = $PSBoundParameters.$parameter - Actual = $getTargetResource.$parameter - Pass = $false + # Resource is present + Write-Verbose -Message ($script:localizedData.RemovingManagedServiceAccountMessage -f + $AccountType, $ServiceAccountName) + + try + { + Remove-ADServiceAccount @adServiceAccountParameters -Confirm:$false + } + catch + { + $errorMessage = ($script:localizedData.RemovingManagedServiceAccountError -f + $AccountType, $ServiceAccountName) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ } } - } #end foreach PSBoundParameter - - return $compareTargetResource -} #end function Compare-TargetResourceState + else + { + # Resource is absent + Write-Verbose -Message ($script:localizedData.ManagedServiceAccountInDesiredStateMessage -f + $AccountType, $ServiceAccountName) + } + } +} #end function Set-TargetResource Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.schema.mof b/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.schema.mof index 4fd5b245a..31ffe063e 100644 --- a/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.schema.mof +++ b/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.schema.mof @@ -2,16 +2,16 @@ class MSFT_ADManagedServiceAccount : OMI_BaseResource { [Key, Description("Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName'). To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created, the user's SamAccountName and CN cannot be changed.")] String ServiceAccountName; - [Write, Description("Specifies whether the user account is created or deleted. If not specified, this value defaults to Present."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; - [Write, Description("The type of managed service account. Single will create a Single Managed Service Account (sMSA) and Group will create a Group Managed Service Account (gMSA). If not specified, this value defaults to Single."), ValueMap{"Group","Single"}, Values{"Group","Single"}] String AccountType; - [Write, Description("Specifies whether or not to remove the service account and recreate it when going from Single Managed Service Account to Group Managed Service Account and vice-versa. If not specified, this value defaults to $false.")] Boolean AccountTypeForce; - [Write, Description("Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. Specified as a Distinguished Name (DN).")] String Path; - [Write, Description("Specifies a description of the object (ldapDisplayName 'description').")] String Description; - [Write, Description("Specifies the display name of the object (ldapDisplayName 'displayName').")] String DisplayName; - [Write, Description("Specifies the members of the object (ldapDisplayName 'PrincipalsAllowedToRetrieveManagedPassword'). Only used when 'Group' is selected for 'AccountType'.")] String Members[]; - [Write, Description("Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSA). If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'. Default value is 'SamAccountName'."), ValueMap{"SamAccountName","DistinguishedName","ObjectGUID","SID"}, Values{"SamAccountName","DistinguishedName","ObjectGUID","SID"}] String MembershipAttribute; + [Required, Description("The type of managed service account. Standalone will create a Standalone Managed Service Account (sMSA) and Group will create a Group Managed Service Account (gMSA)."), ValueMap{"Group","Standalone"}, Values{"Group","Standalone"}] String AccountType; [Write, Description("Specifies the user account credentials to use to perform this task. This is only required if not executing the task on a domain controller or using the parameter DomainController."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Specifies the description of the account (ldapDisplayName 'description').")] String Description; + [Write, Description("Specifies the display name of the account (ldapDisplayName 'displayName').")] String DisplayName; [Write, Description("Specifies the Active Directory Domain Controller instance to use to perform the task. This is only required if not executing the task on a domain controller.")] String DomainController; + [Write, Description("Specifies whether the user account is created or deleted. If not specified, this value defaults to Present."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Specifies which Kerberos encryption types the account supports when creating service tickets. This value sets the encryption types supported flags of the Active Directory msDS-SupportedEncryptionTypes attribute."),ValueMap{"None","RC4","AES128","AES256"}, Values{"None","RC4","AES128","AES256"}] String KerberosEncryptionType[]; + [Write, Description("Specifies the membership policy for systems which can use a group managed service account. (ldapDisplayName 'msDS-GroupMSAMembership'). Only used when 'Group' is selected for 'AccountType'.")] String ManagedPasswordPrincipals[]; + [Write, Description("Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSA). If not specified, this value defaults to SamAccountName."), ValueMap{"SamAccountName","DistinguishedName","ObjectGUID","ObjectSid"}, Values{"SamAccountName","DistinguishedName","ObjectGUID","ObjectSid"}] String MembershipAttribute; + [Write, Description("Specifies the X.500 path of the Organizational Unit (OU) or container where the new account is created. Specified as a Distinguished Name (DN).")] String Path; [Read, Description("Returns whether the user account is enabled or disabled.")] Boolean Enabled; [Read, Description("Returns the Distinguished Name of the Service Account.")] String DistinguishedName; }; diff --git a/DSCResources/MSFT_ADManagedServiceAccount/en-US/MSFT_ADManagedServiceAccount.strings.psd1 b/DSCResources/MSFT_ADManagedServiceAccount/en-US/MSFT_ADManagedServiceAccount.strings.psd1 index aebc17187..afcc58baf 100644 --- a/DSCResources/MSFT_ADManagedServiceAccount/en-US/MSFT_ADManagedServiceAccount.strings.psd1 +++ b/DSCResources/MSFT_ADManagedServiceAccount/en-US/MSFT_ADManagedServiceAccount.strings.psd1 @@ -1,16 +1,22 @@ # culture='en-US' ConvertFrom-StringData @' - AddingManagedServiceAccount = Adding AD Managed Service Account '{0}'. (MSA0001) - RemovingManagedServiceAccount = Removing AD Managed Service Account '{0}'. (MSA0003) - MovingManagedServiceAccount = Moving AD Managed Service Account '{0}' to '{1}'. (MSA0004) - ManagedServiceAccountNotFound = AD Managed Service Account '{0}' was not found. (MSA0005) - RetrievingServiceAccount = Retrieving AD Managed Service Account '{0}'. (MSA0006) - AccountTypeForceNotTrue = The 'AccountTypeForce' was either not specified or set to false. To convert from a '{0}' MSA to a '{1}' MSA, AccountTypeForce must be set to true. (MSA0007) - NotDesiredPropertyState = AD Managed Service Account '{0}' is not correct. Expected '{1}', actual '{2}'. (MSA0008) - MSAInDesiredState = AD Managed Service Account '{0}' is in the desired state. (MSA0009) - MSANotInDesiredState = AD Managed Service Account '{0}' is NOT in the desired state. (MSA0010) - UpdatingManagedServiceAccountProperty = Updating AD Managed Service Account property '{0}' to '{1}'. (MSA0011) - AddingManagedServiceAccountError = Error adding AD Managed Service Account '{0}'. (MSA0012) - RetrievingPrincipalMembers = Retrieving Principals Allowed To Retrieve Managed Password based on '{0}' property. (MSA0013) - RetrievingServiceAccountError = There was an error when retrieving the AD Managed Service Account '{0}'. (MSA0014) + AddingManagedServiceAccountMessage = Adding {0} Account '{1}' to '{2}'. (MSA0001) + RecreatingManagedServiceAccountMessage = Recreating {0} Account '{1}'. (MSA0002) + RemovingManagedServiceAccountMessage = Removing {0} Account '{1}'. (MSA0003) + MovingManagedServiceAccountMessage = Moving {0} Account '{1}' from '{2}' to '{3}'. (MSA0004) + ManagedServiceAccountNotFoundMessage = {0} Account '{1}' was not found. (MSA0005) + RetrievingManagedServiceAccountMessage = Retrieving Account '{0}'. (MSA0006) + ManagedServiceAccountInDesiredStateMessage = {0} Account '{1}' is in the desired state. (MSA0007) + UpdatingManagedServiceAccountPropertyMessage = Updating {0} Account '{1}' property '{2}' to '{3}'. (MSA0008) + RetrievingManagedPasswordPrincipalsMessage = Retrieving Principals Allowed To Retrieve Managed Password based on '{0}' property. (MSA0009) + ResourceExistsButShouldNotMessage = {0} Account '{1}' exists but should not. (MSA0010) + ResourceDoesNotExistButShouldMessage = {0} Account '{1}' does not exist but should. (MSA0011) + AddingManagedServiceAccountError = Error adding {0} Account '{1}' to '{2}'. (MSA0012) + RemovingManagedServiceAccountError = Error removing {0} Account '{1}'. (MSA0013) + SettingManagedServiceAccountError = Error setting {0} Account '{1}'. (MSA0014) + MovingManagedServiceAccountError = Error moving {0} Account '{1}' from '{2}' to '{3}'. (MSA0015) + RetrievingManagedServiceAccountError = Error retrieving Account '{0}'. (MSA0016) + RetrievingManagedPasswordPrincipalsError = Error retrieving Principal '{0}'. (MSA0017) + GettingADDomainError = Error getting Active Directory Domain details. (MSA0018) + KdsRootKeyNotFoundError = Error adding group account '{0}'. The KDS Root Key was not found. (MSA0019) '@ diff --git a/DSCResources/MSFT_ADManagedServiceAccount/en-US/about_ADManagedServiceAccount.help.txt b/DSCResources/MSFT_ADManagedServiceAccount/en-US/about_ADManagedServiceAccount.help.txt index 9eb5b9b97..411880553 100644 --- a/DSCResources/MSFT_ADManagedServiceAccount/en-US/about_ADManagedServiceAccount.help.txt +++ b/DSCResources/MSFT_ADManagedServiceAccount/en-US/about_ADManagedServiceAccount.help.txt @@ -14,48 +14,49 @@ Key - String Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName'). To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created, the user's SamAccountName and CN cannot be changed. -.PARAMETER Ensure +.PARAMETER AccountType + Required - String + Allowed values: Group, Standalone + The type of managed service account. Standalone will create a Standalone Managed Service Account (sMSA) and Group will create a Group Managed Service Account (gMSA). + +.PARAMETER Credential Write - String - Allowed values: Present, Absent - Specifies whether the user account is created or deleted. If not specified, this value defaults to Present. + Specifies the user account credentials to use to perform this task. This is only required if not executing the task on a domain controller or using the parameter DomainController. -.PARAMETER AccountType +.PARAMETER Description Write - String - Allowed values: Group, Single - The type of managed service account. Single will create a Single Managed Service Account (sMSA) and Group will create a Group Managed Service Account (gMSA). If not specified, this value defaults to Single. + Specifies the description of the account (ldapDisplayName 'description'). -.PARAMETER AccountTypeForce - Write - Boolean - Specifies whether or not to remove the service account and recreate it when going from Single Managed Service Account to Group Managed Service Account and vice-versa. If not specified, this value defaults to $false. +.PARAMETER DisplayName + Write - String + Specifies the display name of the account (ldapDisplayName 'displayName'). -.PARAMETER Path +.PARAMETER DomainController Write - String - Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. Specified as a Distinguished Name (DN). + Specifies the Active Directory Domain Controller instance to use to perform the task. This is only required if not executing the task on a domain controller. -.PARAMETER Description +.PARAMETER Ensure Write - String - Specifies a description of the object (ldapDisplayName 'description'). + Allowed values: Present, Absent + Specifies whether the user account is created or deleted. If not specified, this value defaults to Present. -.PARAMETER DisplayName +.PARAMETER KerberosEncryptionType Write - String - Specifies the display name of the object (ldapDisplayName 'displayName'). + Allowed values: None, RC4, AES128, AES256 + Specifies which Kerberos encryption types the account supports when creating service tickets. This value sets the encryption types supported flags of the Active Directory msDS-SupportedEncryptionTypes attribute. -.PARAMETER Members +.PARAMETER ManagedPasswordPrincipals Write - String - Specifies the members of the object (ldapDisplayName 'PrincipalsAllowedToRetrieveManagedPassword'). Only used when 'Group' is selected for 'AccountType'. + Specifies the membership policy for systems which can use a group managed service account. (ldapDisplayName 'msDS-GroupMSAMembership'). Only used when 'Group' is selected for 'AccountType'. .PARAMETER MembershipAttribute Write - String Allowed values: SamAccountName, DistinguishedName, ObjectGUID, SID - Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSA). If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'. Default value is 'SamAccountName'. + Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSA). If not specified, this value defaults to SamAccountName. -.PARAMETER Credential - Write - String - Specifies the user account credentials to use to perform this task. This is only required if not executing the task on a domain controller or using the parameter DomainController. - -.PARAMETER DomainController +.PARAMETER Path Write - String - Specifies the Active Directory Domain Controller instance to use to perform the task. This is only required if not executing the task on a domain controller. + Specifies the X.500 path of the Organizational Unit (OU) or container where the new account is created. Specified as a Distinguished Name (DN). .PARAMETER Enabled Read - Boolean @@ -67,7 +68,7 @@ .EXAMPLE 1 -This configuration will create a managed service account. +This configuration will create a standalone managed service account. Configuration ADManagedServiceAccount_CreateManagedServiceAccount_Config { @@ -75,10 +76,11 @@ Configuration ADManagedServiceAccount_CreateManagedServiceAccount_Config Node localhost { - ADManagedServiceAccount 'ExampleSingleMSA' + ADManagedServiceAccount 'ExampleStandaloneMSA' { Ensure = 'Present' ServiceAccountName = 'Service01' + AccountType = 'Standalone' } } } @@ -98,7 +100,6 @@ Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccount_Config Ensure = 'Present' ServiceAccountName = 'Service01' AccountType = 'Group' - Path = 'OU=ServiceAccounts,DC=contoso,DC=com' } } } @@ -118,7 +119,6 @@ Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccountWithMember Ensure = 'Present' ServiceAccountName = 'Service01' AccountType = 'Group' - Path = 'OU=ServiceAccounts,DC=contoso,DC=com' Members = 'User01', 'Computer01$' } @@ -127,7 +127,6 @@ Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccountWithMember Ensure = 'Present' ServiceAccountName = 'Service02' AccountType = 'Group' - Path = 'OU=ServiceAccounts,DC=contoso,DC=com' Members = 'CN=User01,OU=Users,DC=contoso,DC=com', 'CN=Computer01,OU=Computers,DC=contoso,DC=com' } } diff --git a/Examples/Resources/ADManagedServiceAccount/1-ADManagedServiceAccount_CreateManagedServiceAccount_Config.ps1 b/Examples/Resources/ADManagedServiceAccount/1-ADManagedServiceAccount_CreateManagedServiceAccount_Config.ps1 index 3c47008e4..168f8fb7a 100644 --- a/Examples/Resources/ADManagedServiceAccount/1-ADManagedServiceAccount_CreateManagedServiceAccount_Config.ps1 +++ b/Examples/Resources/ADManagedServiceAccount/1-ADManagedServiceAccount_CreateManagedServiceAccount_Config.ps1 @@ -19,7 +19,8 @@ <# .DESCRIPTION - This configuration will create a managed service account. + This configuration will create a standalone managed service account in the default 'Managed Service Accounts' + container. #> Configuration ADManagedServiceAccount_CreateManagedServiceAccount_Config { @@ -27,10 +28,11 @@ Configuration ADManagedServiceAccount_CreateManagedServiceAccount_Config Node localhost { - ADManagedServiceAccount 'ExampleSingleMSA' + ADManagedServiceAccount 'ExampleStandaloneMSA' { Ensure = 'Present' ServiceAccountName = 'Service01' + AccountType = 'Standalone' } } } diff --git a/Examples/Resources/ADManagedServiceAccount/2-ADManagedServiceAccount_CreateGroupManagedServiceAccount_Config.ps1 b/Examples/Resources/ADManagedServiceAccount/2-ADManagedServiceAccount_CreateGroupManagedServiceAccount_Config.ps1 index 8e6d115d3..63eb21c1e 100644 --- a/Examples/Resources/ADManagedServiceAccount/2-ADManagedServiceAccount_CreateGroupManagedServiceAccount_Config.ps1 +++ b/Examples/Resources/ADManagedServiceAccount/2-ADManagedServiceAccount_CreateGroupManagedServiceAccount_Config.ps1 @@ -19,7 +19,8 @@ <# .DESCRIPTION - This configuration will create a group managed service account. + This configuration will create a group managed service account in the default 'Managed Service Accounts' + container. #> Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccount_Config { @@ -32,7 +33,6 @@ Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccount_Config Ensure = 'Present' ServiceAccountName = 'Service01' AccountType = 'Group' - Path = 'OU=ServiceAccounts,DC=contoso,DC=com' } } } diff --git a/Examples/Resources/ADManagedServiceAccount/3-ADManagedServiceAccount_CreateGroupManagedServiceAccountWithMembers_Config.ps1 b/Examples/Resources/ADManagedServiceAccount/3-ADManagedServiceAccount_CreateGroupManagedServiceAccountWithMembers_Config.ps1 index f2debd498..54b72df16 100644 --- a/Examples/Resources/ADManagedServiceAccount/3-ADManagedServiceAccount_CreateGroupManagedServiceAccountWithMembers_Config.ps1 +++ b/Examples/Resources/ADManagedServiceAccount/3-ADManagedServiceAccount_CreateGroupManagedServiceAccountWithMembers_Config.ps1 @@ -19,7 +19,8 @@ <# .DESCRIPTION - This configuration will create a group managed service account with members. + This configuration will create a group managed service account with members in the default 'Managed Service + Accounts' container. #> Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccountWithMembers_Config { @@ -29,20 +30,18 @@ Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccountWithMember { ADManagedServiceAccount 'AddingMembersUsingSamAccountName' { - Ensure = 'Present' - ServiceAccountName = 'Service01' - AccountType = 'Group' - Path = 'OU=ServiceAccounts,DC=contoso,DC=com' - Members = 'User01', 'Computer01$' + Ensure = 'Present' + ServiceAccountName = 'Service01' + AccountType = 'Group' + ManagedPasswordPrincipals = 'User01', 'Computer01$' } ADManagedServiceAccount 'AddingMembersUsingDN' { - Ensure = 'Present' - ServiceAccountName = 'Service02' - AccountType = 'Group' - Path = 'OU=ServiceAccounts,DC=contoso,DC=com' - Members = 'CN=User01,OU=Users,DC=contoso,DC=com', 'CN=Computer01,OU=Computers,DC=contoso,DC=com' + Ensure = 'Present' + ServiceAccountName = 'Service02' + AccountType = 'Group' + ManagedPasswordPrincipals = 'CN=User01,OU=Users,DC=contoso,DC=com', 'CN=Computer01,OU=Computers,DC=contoso,DC=com' } } } diff --git a/Examples/Resources/ADManagedServiceAccount/4-ADManagedServiceAccount_CreateGroupManagedServiceAccountCustomPath_Config.ps1 b/Examples/Resources/ADManagedServiceAccount/4-ADManagedServiceAccount_CreateGroupManagedServiceAccountCustomPath_Config.ps1 new file mode 100644 index 000000000..0f0a23a27 --- /dev/null +++ b/Examples/Resources/ADManagedServiceAccount/4-ADManagedServiceAccount_CreateGroupManagedServiceAccountCustomPath_Config.ps1 @@ -0,0 +1,41 @@ +<#PSScriptInfo +.VERSION 1.0 +.GUID f758390b-0576-416a-9110-a0b26263415e +.AUTHOR Microsoft Corporation +.COMPANYNAME Microsoft Corporation +.COPYRIGHT (c) Microsoft Corporation. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/PowerShell/ActiveDirectoryDsc/blob/master/LICENSE +.PROJECTURI https://github.com/PowerShell/ActiveDirectoryDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES +.PRIVATEDATA +#> + +#Requires -module ActiveDirectoryDsc + +<# + .DESCRIPTION + This configuration will create a group managed service account in the specified path. +#> +Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccountCustomPath_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + Node localhost + { + ADManagedServiceAccount 'ExampleGroupMSA' + { + Ensure = 'Present' + ServiceAccountName = 'Service01' + AccountType = 'Group' + Path = 'OU=ServiceAccounts,DC=contoso,DC=com' + } + } + } +} diff --git a/Tests/Integration/MSFT_ADManagedServiceAccount.Integration.Tests.ps1 b/Tests/Integration/MSFT_ADManagedServiceAccount.Integration.Tests.ps1 new file mode 100644 index 000000000..23e9039b7 --- /dev/null +++ b/Tests/Integration/MSFT_ADManagedServiceAccount.Integration.Tests.ps1 @@ -0,0 +1,370 @@ +if ($env:APPVEYOR -eq $true) +{ + Write-Warning -Message 'Integration test is not supported in AppVeyor.' + return +} + +$script:dscModuleName = 'ActiveDirectoryDsc' +$script:dscResourceFriendlyName = 'ADManagedServiceAccount' +$script:dscResourceName = "MSFT_$($script:dscResourceFriendlyName)" + +#region HEADER +# Integration Test Template Version: 1.3.3 +[System.String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $script:moduleRoot -ChildPath 'DscResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -TestType Integration +#endregion + +try +{ + $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:dscResourceName).config.ps1" + . $configFile + + Describe "$($script:dscResourceName)_Integration" { + BeforeAll { + $resourceId = "[$($script:dscResourceFriendlyName)]Integration_Test" + + $DefaultManagedServiceAccountPath = "CN=Managed Service Accounts,$($ConfigurationData.AllNodes.DomainDistinguishedName)" + + $DefaultKerberosEncryptionType = 'RC4', 'AES128', 'AES256' + + $configurationParameters = @{ + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + } + + $configurationName = "$($script:dscResourceName)_Initialise_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + & $configurationName @configurationParameters + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + } + + $configurationName = "$($script:dscResourceName)_CreateServiceAccount1_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + & $configurationName @configurationParameters + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Present' + $resourceCurrentState.ServiceAccountName | Should -Be $ConfigurationData.ManagedServiceAccount1.Name + $resourceCurrentState.AccountType | Should -Be $ConfigurationData.ManagedServiceAccount1.AccountType + $resourceCurrentState.Path | Should -Be $DefaultManagedServiceAccountPath + $resourceCurrentState.Description | Should -BeNullOrEmpty + $resourceCurrentState.DisplayName | Should -BeNullOrEmpty + $resourceCurrentState.Enabled | Should -Be $true + $resourceCurrentState.ManagedPasswordPrincipals | Should -BeNullOrEmpty + $resourceCurrentState.MembershipAttribute | Should -Be 'SamAccountName' + $resourceCurrentState.KerberosEncryptionType | Should -Be $DefaultKerberosEncryptionType + $resourceCurrentState.DistinguishedName | Should -Be ('CN={0},CN=Managed Service Accounts,{1}' -f ` + $ConfigurationData.ManagedServiceAccount1.Name, $ConfigurationData.AllNodes.DomainDistinguishedName) + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_CreateServiceAccount2_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + & $configurationName @configurationParameters + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Present' + $resourceCurrentState.ServiceAccountName | Should -Be $ConfigurationData.ManagedServiceAccount2.Name + $resourceCurrentState.AccountType | Should -Be $ConfigurationData.ManagedServiceAccount2.AccountType + $resourceCurrentState.Path | Should -Be $DefaultManagedServiceAccountPath + $resourceCurrentState.Description | Should -BeNullOrEmpty + $resourceCurrentState.DisplayName | Should -BeNullOrEmpty + $resourceCurrentState.Enabled | Should -Be $true + $resourceCurrentState.ManagedPasswordPrincipals | Should -BeNullOrEmpty + $resourceCurrentState.MembershipAttribute | Should -Be 'SamAccountName' + $resourceCurrentState.KerberosEncryptionType | Should -Be $DefaultKerberosEncryptionType + $resourceCurrentState.DistinguishedName | Should -Be ('CN={0},{1}' -f ` + $ConfigurationData.ManagedServiceAccount2.Name, $DefaultManagedServiceAccountPath) + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_CreateServiceAccount3_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + & $configurationName @configurationParameters + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Present' + $resourceCurrentState.ServiceAccountName | Should -Be $ConfigurationData.ManagedServiceAccount3.Name + $resourceCurrentState.AccountType | Should -Be $ConfigurationData.ManagedServiceAccount3.AccountType + $resourceCurrentState.Path | Should -Be $DefaultManagedServiceAccountPath + $resourceCurrentState.Description | Should -BeNullOrEmpty + $resourceCurrentState.DisplayName | Should -BeNullOrEmpty + $resourceCurrentState.Enabled | Should -Be $true + $resourceCurrentState.ManagedPasswordPrincipals | Should -BeNullOrEmpty + $resourceCurrentState.MembershipAttribute | Should -Be 'SamAccountName' + $resourceCurrentState.KerberosEncryptionType | Should -Be $DefaultKerberosEncryptionType + $resourceCurrentState.DistinguishedName | Should -Be ('CN={0},{1}' -f $ConfigurationData.ManagedServiceAccount3.Name, ` + $DefaultManagedServiceAccountPath) + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_RemoveServiceAccount1_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + & $configurationName @configurationParameters + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Absent' + $resourceCurrentState.ServiceAccountName | Should -Be $ConfigurationData.ManagedServiceAccount1.Name + $resourceCurrentState.AccountType | Should -Be $ConfigurationData.ManagedServiceAccount1.AccountType + $resourceCurrentState.Path | Should -BeNullOrEmpty + $resourceCurrentState.Description | Should -BeNullOrEmpty + $resourceCurrentState.DisplayName | Should -BeNullOrEmpty + $resourceCurrentState.Enabled | Should -BeFalse + $resourceCurrentState.ManagedPasswordPrincipals | Should -BeNullOrEmpty + $resourceCurrentState.MembershipAttribute | Should -Be 'SamAccountName' + $resourceCurrentState.KerberosEncryptionType | Should -BeNullOrEmpty + $resourceCurrentState.DistinguishedName | Should -BeNullOrEmpty + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_UpdateServiceAccount2_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + & $configurationName @configurationParameters + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Present' + $resourceCurrentState.ServiceAccountName | Should -Be $ConfigurationData.ManagedServiceAccount2.Name + $resourceCurrentState.AccountType | Should -Be $ConfigurationData.ManagedServiceAccount2.AccountType + $resourceCurrentState.Path | Should -Be $ConfigurationData.ManagedServiceAccount2.Path + $resourceCurrentState.Description | Should -Be $ConfigurationData.ManagedServiceAccount2.Description + $resourceCurrentState.DisplayName | Should -Be $ConfigurationData.ManagedServiceAccount2.DisplayName + $resourceCurrentState.Enabled | Should -Be $true + $resourceCurrentState.ManagedPasswordPrincipals | Should -BeNullOrEmpty + $resourceCurrentState.MembershipAttribute | Should -Be 'SamAccountName' + $resourceCurrentState.KerberosEncryptionType | Should -Be $ConfigurationData.ManagedServiceAccount2.KerberosEncryptionType + $resourceCurrentState.DistinguishedName | Should -Be ('CN={0},{1}' -f ` + $ConfigurationData.ManagedServiceAccount2.Name, $ConfigurationData.ManagedServiceAccount2.Path) + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_EnforcePasswordPrincipalsServiceAccount3_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + & $configurationName @configurationParameters + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Present' + $resourceCurrentState.ServiceAccountName | Should -Be $ConfigurationData.ManagedServiceAccount3.Name + $resourceCurrentState.AccountType | Should -Be $ConfigurationData.ManagedServiceAccount3.AccountType + $resourceCurrentState.Path | Should -Be $DefaultManagedServiceAccountPath + $resourceCurrentState.Description | Should -BeNullOrEmpty + $resourceCurrentState.DisplayName | Should -BeNullOrEmpty + $resourceCurrentState.Enabled | Should -Be $true + $resourceCurrentState.ManagedPasswordPrincipals | Should -Be $ConfigurationData.ManagedServiceAccount3.ManagedPasswordPrincipals + $resourceCurrentState.MembershipAttribute | Should -Be 'SamAccountName' + $resourceCurrentState.KerberosEncryptionType | Should -Be $DefaultKerberosEncryptionType + $resourceCurrentState.DistinguishedName | Should -Be ('CN={0},{1}' -f ` + $ConfigurationData.ManagedServiceAccount3.Name, $DefaultManagedServiceAccountPath) + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_ClearPasswordPrincipalsServiceAccount3_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + & $configurationName @configurationParameters + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Present' + $resourceCurrentState.ServiceAccountName | Should -Be $ConfigurationData.ManagedServiceAccount3.Name + $resourceCurrentState.AccountType | Should -Be $ConfigurationData.ManagedServiceAccount3.AccountType + $resourceCurrentState.Path | Should -Be $DefaultManagedServiceAccountPath + $resourceCurrentState.Description | Should -BeNullOrEmpty + $resourceCurrentState.DisplayName | Should -BeNullOrEmpty + $resourceCurrentState.Enabled | Should -Be $true + $resourceCurrentState.ManagedPasswordPrincipals | Should -BeNullOrEmpty + $resourceCurrentState.MembershipAttribute | Should -Be 'SamAccountName' + $resourceCurrentState.KerberosEncryptionType | Should -Be $DefaultKerberosEncryptionType + $resourceCurrentState.DistinguishedName | Should -Be ('CN={0},{1}' -f ` + $ConfigurationData.ManagedServiceAccount3.Name, $DefaultManagedServiceAccountPath) + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_Initialise_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + & $configurationName @configurationParameters + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + } + } +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/Tests/Integration/MSFT_ADManagedServiceAccount.config.ps1 b/Tests/Integration/MSFT_ADManagedServiceAccount.config.ps1 new file mode 100644 index 000000000..f817ccd8f --- /dev/null +++ b/Tests/Integration/MSFT_ADManagedServiceAccount.config.ps1 @@ -0,0 +1,218 @@ +#region HEADER +# Integration Test Config Template Version: 1.2.0 +#endregion + +$configFile = [System.IO.Path]::ChangeExtension($MyInvocation.MyCommand.Path, 'json') +if (Test-Path -Path $configFile) +{ + <# + Allows reading the configuration data from a JSON file, for real testing + scenarios outside of the CI. + #> + $ConfigurationData = Get-Content -Path $configFile | ConvertFrom-Json +} +else +{ + $currentDomain = Get-ADDomain + $domainDistinguishedName = $currentDomain.DistinguishedName + + $ConfigurationData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + CertificateFile = $env:DscPublicCertificatePath + DomainDistinguishedName = $domainDistinguishedName + } + ) + + ManagedServiceAccount1 = @{ + Name = 'Dsc-sMSA1' + AccountType = 'Standalone' + } + + ManagedServiceAccount2 = @{ + Name = 'Dsc-gMSA1' + AccountType = 'Group' + Path = "CN=Users,$($domainDistinguishedName)" + DisplayName = 'DSC Group Managed Service Account 2' + Description = 'A DSC description' + KerberosEncryptionType = 'AES128', 'AES256' + } + + ManagedServiceAccount3 = @{ + Name = 'Dsc-gMSA2' + AccountType = 'Group' + ManagedPasswordPrincipals = @( + 'Administrator', + 'Guest' + ) + } + } +} + +<# + .SYNOPSIS + Initialise Config +#> +Configuration MSFT_ADManagedServiceAccount_Initialise_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + ADManagedServiceAccount 'RemoveGroup1' + { + ServiceAccountName = $ConfigurationData.ManagedServiceAccount1.Name + AccountType = $ConfigurationData.ManagedServiceAccount1.AccountType + Ensure = 'Absent' + } + + ADManagedServiceAccount 'RemoveGroup2' + { + ServiceAccountName = $ConfigurationData.ManagedServiceAccount2.Name + AccountType = $ConfigurationData.ManagedServiceAccount2.AccountType + Ensure = 'Absent' + } + + ADManagedServiceAccount 'RemoveGroup3' + { + ServiceAccountName = $ConfigurationData.ManagedServiceAccount3.Name + AccountType = $ConfigurationData.ManagedServiceAccount3.AccountType + Ensure = 'Absent' + } + } +} + +<# + .SYNOPSIS + Add a Stand-Alone ManagedServiceAccount using default values. +#> +Configuration MSFT_ADManagedServiceAccount_CreateServiceAccount1_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + ADManagedServiceAccount 'Integration_Test' + { + ServiceAccountName = $ConfigurationData.ManagedServiceAccount1.Name + AccountType = $ConfigurationData.ManagedServiceAccount1.AccountType + } + } +} + +<# + .SYNOPSIS + Add a Group ManagedServiceAccount using default values. +#> +Configuration MSFT_ADManagedServiceAccount_CreateServiceAccount2_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + ADManagedServiceAccount 'Integration_Test' + { + ServiceAccountName = $ConfigurationData.ManagedServiceAccount2.Name + AccountType = $ConfigurationData.ManagedServiceAccount2.AccountType + } + } +} + +<# + .SYNOPSIS + Add a Second Group ManagedServiceAccount using default values. +#> +Configuration MSFT_ADManagedServiceAccount_CreateServiceAccount3_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + ADManagedServiceAccount 'Integration_Test' + { + ServiceAccountName = $ConfigurationData.ManagedServiceAccount3.Name + AccountType = $ConfigurationData.ManagedServiceAccount3.AccountType + } + } +} + +<# + .SYNOPSIS + Remove a group. +#> +Configuration MSFT_ADManagedServiceAccount_RemoveServiceAccount1_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + ADManagedServiceAccount 'Integration_Test' + { + ServiceAccountName = $ConfigurationData.ManagedServiceAccount1.Name + AccountType = $ConfigurationData.ManagedServiceAccount1.AccountType + Ensure = 'Absent' + } + } +} + + +<# + .SYNOPSIS + Update an existing group. +#> +Configuration MSFT_ADManagedServiceAccount_UpdateServiceAccount2_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + ADManagedServiceAccount 'Integration_Test' + { + ServiceAccountName = $ConfigurationData.ManagedServiceAccount2.Name + AccountType = $ConfigurationData.ManagedServiceAccount2.AccountType + Path = $ConfigurationData.ManagedServiceAccount2.Path + DisplayName = $ConfigurationData.ManagedServiceAccount2.DisplayName + Description = $ConfigurationData.ManagedServiceAccount2.Description + KerberosEncryptionType = $ConfigurationData.ManagedServiceAccount2.KerberosEncryptionType + } + } +} + +<# + .SYNOPSIS + Enforce members in a group. +#> +Configuration MSFT_ADManagedServiceAccount_EnforcePasswordPrincipalsServiceAccount3_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + ADManagedServiceAccount 'Integration_Test' + { + ServiceAccountName = $ConfigurationData.ManagedServiceAccount3.Name + AccountType = $ConfigurationData.ManagedServiceAccount3.AccountType + ManagedPasswordPrincipals = $ConfigurationData.ManagedServiceAccount3.ManagedPasswordPrincipals + } + } +} + +<# + .SYNOPSIS + Enforce no members in a group. +#> +Configuration MSFT_ADManagedServiceAccount_ClearPasswordPrincipalsServiceAccount3_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + ADManagedServiceAccount 'Integration_Test' + { + ServiceAccountName = $ConfigurationData.ManagedServiceAccount3.Name + AccountType = $ConfigurationData.ManagedServiceAccount3.AccountType + ManagedPasswordPrincipals = @() + } + } +} diff --git a/Tests/Unit/MSFT_ADManagedServiceAccount.Tests.ps1 b/Tests/Unit/MSFT_ADManagedServiceAccount.Tests.ps1 index 2ed802343..0386dab6f 100644 --- a/Tests/Unit/MSFT_ADManagedServiceAccount.Tests.ps1 +++ b/Tests/Unit/MSFT_ADManagedServiceAccount.Tests.ps1 @@ -1,3 +1,7 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', "", + Justification = 'False positive on ManagedPasswordPrincipals')] +param () + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\ActiveDirectoryDsc.TestHelper.psm1') if (-not (Test-RunForCITestCategory -Type 'Unit' -Category 'Tests')) @@ -28,10 +32,6 @@ $TestEnvironment = Initialize-TestEnvironment ` #endregion HEADER -function Invoke-TestSetup -{ -} - function Invoke-TestCleanup { Restore-TestEnvironment -TestEnvironment $TestEnvironment @@ -40,1537 +40,820 @@ function Invoke-TestCleanup # Begin Testing try { - Invoke-TestSetup - InModuleScope $script:dscResourceName { # Load stub cmdlets and classes. Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'Stubs\ActiveDirectory_2019.psm1') -Force - # Need to do a deep copy of the Array of objects that compare returns - function Copy-ArrayObjects - { - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.Array] - $DeepCopyObject - ) - - $memStream = New-Object -TypeName 'IO.MemoryStream' - $formatter = New-Object -TypeName 'Runtime.Serialization.Formatters.Binary.BinaryFormatter' - $formatter.Serialize($memStream,$DeepCopyObject) - $memStream.Position=0 - $formatter.Deserialize($memStream) - } + $mockDefaultMsaPath = 'CN=Managed Service Accounts,DC=contoso,DC=com' + $mockChangedPath = 'OU=Service Accounts,DC=contoso,DC=com' + $mockDomainController = 'MockDC' + $mockDomainName = 'contoso.com' - $mockPath = 'OU=Fake,DC=contoso,DC=com' - $mockDomainController = 'MockDC' - - $mockCredentials = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @( + $mockCredentials = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @( 'DummyUser', (ConvertTo-SecureString -String 'DummyPassword' -AsPlainText -Force) ) $mockADUSer = @{ - SamAccountName = 'User1' - DistinguishedName = 'CN=User1,OU=Fake,DC=contoso,DC=com' - Enabled = $true - SID = 'S-1-5-21-1409167834-891301383-2860967316-1142' - ObjectGUID = '91bffe90-4c84-4026-b1fc-d03671ff56ab' - GivenName = '' + DistinguishedName = 'CN=User1,CN=Users,DC=contoso,DC=com' Name = 'User1' + ObjectClass = 'user' + ObjectGUID = '91bffe90-4c84-4026-b1fc-d03671ff56ab' + ObjectSid = 'S-1-5-21-1409167834-891301383-2860967316-1142' + SamAccountName = 'User1' } $mockADComputer = @{ - SamAccountName = 'Node1$' - DistinguishedName = 'CN=Node1,OU=Fake,DC=contoso,DC=com' - Enabled = $true - SID = 'S-1-5-21-1409167834-891301383-2860967316-1143' + DistinguishedName = 'CN=Node1,CN=Computers,DC=contoso,DC=com' + Name = 'Node1' ObjectClass = 'computer' ObjectGUID = '91bffe90-4c84-4026-b1fc-d03671ff56ac' - DnsHostName = 'Node1.fake.contoso.com' + ObjectSID = 'S-1-5-21-1409167834-891301383-2860967316-1143' + SamAccountName = 'Node1$' } - $mockSingleServiceAccount = @{ - Name = 'TestSMSA' - DistinguishedName = "CN={0},{1}" -f ('TestSMSA', $mockPath) - Description = 'Dummy single service account for unit testing' - DisplayName = '' - ObjectClass = 'msDS-ManagedServiceAccount' - Enabled = $true - SamAccountName = 'TestSMSA' - SID = 'S-1-5-21-1409167834-891301383-2860967316-1144' - ObjectGUID = '91bffe90-4c84-4026-b1fc-d03671ff56ad' + $mockAdServiceAccountStandalone = @{ + ServiceAccountName = 'TestSMSA' + AccountType = 'Standalone' + DistinguishedName = "CN=TestSMSA,$mockDefaultMsaPath" + Description = 'Dummy StandAlone service account for unit testing' + DisplayName = 'TestSMSA' + Enabled = $true + KerberosEncryptionType = 'RC4', 'AES128', 'AES256' + ManagedPasswordPrincipals = @() + MembershipAttribute = 'SamAccountName' + Ensure = 'Present' } - $mockGroupServiceAccount = @{ - Name = 'TestGMSA' - DistinguishedName = "CN={0},{1}" -f ('TestGMSA', $mockPath) - Description = 'Dummy group service account for unit testing' - DisplayName = '' - ObjectClass = 'msDS-GroupManagedServiceAccount' - Enabled = $true - SID = 'S-1-5-21-1409167834-891301383-2860967316-1145' - ObjectGUID = '91bffe90-4c84-4026-b1fc-d03671ff56ae' - PrincipalsAllowedToRetrieveManagedPassword = @($mockADUSer.SamAccountName, $mockADComputer.SamAccountName) + $mockAdServiceAccountStandaloneAbsent = @{ + ServiceAccountName = $mockAdServiceAccountStandalone.ServiceAccountName + AccountType = $mockAdServiceAccountStandalone.AccountType + DistinguishedName = $null + Description = $null + DisplayName = $null + Enabled = $false + ManagedPasswordPrincipals = @() + MembershipAttribute = $mockAdServiceAccountStandalone.MembershipAttribute + KerberosEncryptionType = @() + Ensure = 'Absent' } - $mockGetSingleServiceAccount = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - DistinguishedName = $mockSingleServiceAccount.DistinguishedName - Path = $mockPath - Description = $mockSingleServiceAccount.Description - DisplayName = $mockSingleServiceAccount.DisplayName - AccountType = 'Single' - AccountTypeForce = $false - Ensure = 'Present' - Enabled = $true - Members = @() - MembershipAttribute = 'sAMAccountName' - Credential = $mockCredentials - DomainController = $mockDomainController + $mockAdServiceAccountChanged = @{ + Description = 'Changed description' + DisplayName = 'Changed displayname' + KerberosEncryptionType = 'AES128', 'AES256' + ManagedPasswordPrincipals = $mockADUSer.SamAccountName } - $mockGetGroupServiceAccount = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - DistinguishedName = $mockGroupServiceAccount.DistinguishedName - Path = $mockPath - Description = $mockGroupServiceAccount.Description - DisplayName = $mockGroupServiceAccount.DisplayName - AccountType = 'Group' - AccountTypeForce = $false - Ensure = 'Present' - Enabled = $true - Members = $mockGroupServiceAccount.PrincipalsAllowedToRetrieveManagedPassword - MembershipAttribute = 'sAMAccountName' - Credential = $mockCredentials - DomainController = $mockDomainController + $mockAdServiceAccountGroup = @{ + ServiceAccountName = 'TestGMSA' + AccountType = 'Group' + DistinguishedName = "CN=TestGMSA,$mockDefaultMsaPath" + Description = 'Dummy group service account for unit testing' + DisplayName = 'TestGMSA' + Enabled = $true + KerberosEncryptionType = 'RC4', 'AES128', 'AES256' + ManagedPasswordPrincipals = $mockADUSer.SamAccountName, $mockADComputer.SamAccountName + MembershipAttribute = 'SamAccountName' + Ensure = 'Present' } - $mockCompareSingleServiceAccount = @( - [pscustomobject] @{ - Parameter = 'ServiceAccountName' - Expected = $mockGetSingleServiceAccount.ServiceAccountName - Actual = $mockGetSingleServiceAccount.ServiceAccountName - Pass = $true - } - [pscustomobject] @{ - Parameter = 'AccountType' - Expected = $mockGetSingleServiceAccount.AccountType - Actual = $mockGetSingleServiceAccount.AccountType - Pass = $true - } - [pscustomobject] @{ - Parameter = 'AccountTypeForce' - Expected = $mockGetSingleServiceAccount.AccountTypeForce - Actual = $mockGetSingleServiceAccount.AccountTypeForce - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Path' - Expected = $mockGetSingleServiceAccount.Path - Actual = $mockGetSingleServiceAccount.Path - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Ensure' - Expected = $mockGetSingleServiceAccount.Ensure - Actual = $mockGetSingleServiceAccount.Ensure - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Enabled' - Expected = $mockGetSingleServiceAccount.Enabled - Actual = $mockGetSingleServiceAccount.Enabled - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Description' - Expected = $mockGetSingleServiceAccount.Description - Actual = $mockGetSingleServiceAccount.Description - Pass = $true - } - [pscustomobject] @{ - Parameter = 'DisplayName' - Expected = $mockGetSingleServiceAccount.DisplayName - Actual = $mockGetSingleServiceAccount.DisplayName - Pass = $true - } - [pscustomobject] @{ - Parameter = 'DistinguishedName' - Expected = $mockGetSingleServiceAccount.DistinguishedName - Actual = $mockGetSingleServiceAccount.DistinguishedName - Pass = $true - } - [pscustomobject] @{ - Parameter = 'MembershipAttribute' - Expected = $mockGetSingleServiceAccount.MembershipAttribute - Actual = $mockGetSingleServiceAccount.MembershipAttribute - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Credential' - Expected = $mockGetSingleServiceAccount.Credential - Actual = $mockGetSingleServiceAccount.Credential - Pass = $true - } - [pscustomobject] @{ - Parameter = 'DomainController' - Expected = $mockGetSingleServiceAccount.DomainController - Actual = $mockGetSingleServiceAccount.DomainController - Pass = $true - } - ) - - $mockCompareGroupServiceAccount = @( - [pscustomobject] @{ - Parameter = 'ServiceAccountName' - Expected = $mockGetGroupServiceAccount.ServiceAccountName - Actual = $mockGetGroupServiceAccount.ServiceAccountName - Pass = $true - } - [pscustomobject] @{ - Parameter = 'AccountType' - Expected = $mockGetGroupServiceAccount.AccountType - Actual = $mockGetGroupServiceAccount.AccountType - Pass = $true - } - [pscustomobject] @{ - Parameter = 'AccountTypeForce' - Expected = $mockGetGroupServiceAccount.AccountTypeForce - Actual = $mockGetGroupServiceAccount.AccountTypeForce - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Path' - Expected = $mockGetGroupServiceAccount.Path - Actual = $mockGetGroupServiceAccount.Path - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Ensure' - Expected = $mockGetGroupServiceAccount.Ensure - Actual = $mockGetGroupServiceAccount.Ensure - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Enabled' - Expected = $mockGetGroupServiceAccount.Enabled - Actual = $mockGetGroupServiceAccount.Enabled - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Description' - Expected = $mockGetGroupServiceAccount.Description - Actual = $mockGetGroupServiceAccount.Description - Pass = $true - } - [pscustomobject] @{ - Parameter = 'DisplayName' - Expected = $mockGetGroupServiceAccount.DisplayName - Actual = $mockGetGroupServiceAccount.DisplayName - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Members' - Expected = $mockGetGroupServiceAccount.Members - Actual = $mockGetGroupServiceAccount.Members - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Credential' - Expected = $mockGetGroupServiceAccount.MembershipAttribute - Actual = $mockGetGroupServiceAccount.MembershipAttribute - Pass = $true - } - [pscustomobject] @{ - Parameter = 'DistinguishedName' - Expected = $mockGetGroupServiceAccount.DistinguishedName - Actual = $mockGetGroupServiceAccount.DistinguishedName - Pass = $true - } - [pscustomobject] @{ - Parameter = 'Credential' - Expected = $mockGetGroupServiceAccount.Credential - Actual = $mockGetGroupServiceAccount.Credential - Pass = $true - } - [pscustomobject] @{ - Parameter = 'DomainController' - Expected = $mockGetGroupServiceAccount.DomainController - Actual = $mockGetGroupServiceAccount.DomainController - Pass = $true - } - ) - - #region Function Get-TargetResource - Describe -Name 'MSFT_ADManagedServiceAccount\Get-TargetResource' -Tag 'Get' { - BeforeAll { - Mock -CommandName Assert-Module -ParameterFilter { - $ModuleName -eq 'ActiveDirectory' - } - - Mock -CommandName Get-ADObjectParentDN -MockWith { - return $mockPath - } - } - - Context 'When the system uses specific parameters' { - Mock -CommandName Get-ADServiceAccount -MockWith { - return $mockSingleServiceAccount - } - - It 'Should call "Assert-Module" to check AD module is installed' { - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - } - - { Get-TargetResource @testResourceParametersSingle } | Should -Not -Throw - - Assert-MockCalled -CommandName Assert-Module -ParameterFilter { - $ModuleName -eq 'ActiveDirectory' - } -Scope It -Exactly -Times 1 - } - - It 'Should call "Get-ADServiceAccount" with "Server" parameter when "DomainController" specified' { - $testResourceParametersWithServer = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - DomainController = $mockDomainController - } - - { Get-TargetResource @testResourceParametersWithServer } | Should -Not -Throw - - Assert-MockCalled -CommandName Get-ADServiceAccount -ParameterFilter { - $Server -eq $mockDomainController - } -Scope It -Exactly -Times 1 - } - - It 'Should call "Get-ADServiceAccount" with "Credential" parameter when specified' { - $testResourceParametersWithCredentials = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - Credential = $mockCredentials - } - - { Get-TargetResource @testResourceParametersWithCredentials } | Should -Not -Throw - - Assert-MockCalled -CommandName Get-ADServiceAccount -ParameterFilter { - $Credential -eq $mockCredentials - } -Scope It -Exactly -Times 1 - } - } - - Context 'When system cannot connect to domain or other errors' { - Mock -CommandName Get-ADServiceAccount -MockWith { - throw 'Microsoft.ActiveDirectory.Management.ADServerDownException' - } - - It 'Should call "Get-ADServiceAccount" and throw an error when catching any other errors besides "Account Not Found"'{ - $getTargetResourceParameters = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - } - - { Get-TargetResource @getTargetResourceParameters -ErrorAction 'SilentlyContinue' } | - Should -Throw ($script:localizedData.RetrievingServiceAccountError -f $getTargetResourceParameters.ServiceAccountName) - } - } - - Context 'When the system is in desired state (sMSA)' { - Mock -CommandName Get-ADServiceAccount -ParameterFilter { - $mockSingleServiceAccount.Name -eq $Identity - } -MockWith { - Write-Verbose "Call Get-ADServiceAccount with $($mockSingleServiceAccount.Name)" - return $mockSingleServiceAccount - } - - It 'Should mock call to Get-ADServiceAccount return identical information' { - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - } - - $getTargetResourceResult = Get-TargetResource @testResourceParametersSingle + $mockAdServiceAccountGroupAbsent = @{ + ServiceAccountName = $mockAdServiceAccountGroup.ServiceAccountName + AccountType = $mockAdServiceAccountGroup.AccountType + DistinguishedName = $null + Description = $null + DisplayName = $null + Enabled = $false + ManagedPasswordPrincipals = @() + MembershipAttribute = $mockAdServiceAccountGroup.MembershipAttribute + KerberosEncryptionType = @() + Ensure = 'Absent' + } - $getTargetResourceResult.ServiceAccountName | Should -Be $mockSingleServiceAccount.Name - $getTargetResourceResult.Ensure | Should -Be 'Present' - $getTargetResourceResult.AccountType | Should -Be 'Single' - $getTargetResourceResult.Description | Should -Be $mockSingleServiceAccount.Description - $getTargetResourceResult.DisplayName | Should -Be $mockSingleServiceAccount.DisplayName - $getTargetResourceResult.Members | Should -Be @() - $getTargetResourceResult.Path | Should -Be $mockPath - } - } + $mockGetAdServiceAccountResultsStandAlone = @{ + Description = $mockAdServiceAccountStandalone.Description + DisplayName = $mockAdServiceAccountStandalone.DisplayName + DistinguishedName = $mockAdServiceAccountStandalone.DistinguishedName + Enabled = $mockAdServiceAccountStandalone.Enabled + KerberosEncryptionType = $mockAdServiceAccountStandalone.KerberosEncryptionType + Name = $mockAdServiceAccountStandalone.ServiceAccountName + ObjectClass = 'msDS-ManagedServiceAccount' + ObjectGUID = '91bffe90-4c84-4026-b1fc-d03671ff56ad' + SamAccountName = $mockAdServiceAccountStandalone.ServiceAccountName + SID = 'S-1-5-21-1409167834-891301383-2860967316-1144' + UserPrincipalName = '' + } - Context 'When the system is in desired state (gMSA)' { - Mock -CommandName Get-ADServiceAccount -ParameterFilter { - $mockGroupServiceAccount.Name -eq $Identity - } -MockWith { - Write-Verbose "Call Get-ADServiceAccount with $($mockGroupServiceAccount.Name)" - return $mockGroupServiceAccount - } + $mockGetAdServiceAccountResultsGroup = @{ + Description = $mockAdServiceAccountGroup.Description + DisplayName = $mockAdServiceAccountGroup.DisplayName + DistinguishedName = $mockAdServiceAccountGroup.DistinguishedName + Enabled = $mockAdServiceAccountGroup.Enabled + KerberosEncryptionType = $mockAdServiceAccountGroup.KerberosEncryptionType + Name = $mockAdServiceAccountGroup.ServiceAccountName + ObjectClass = 'msDS-GroupManagedServiceAccount' + ObjectGUID = '91bffe90-4c84-4026-b1fc-d03671ff56ae' + PrincipalsAllowedToRetrieveManagedPassword = $mockAdServiceAccountGroup.ManagedPasswordPrincipals + SamAccountName = $mockAdServiceAccountGroup.ServiceAccountName + SID = 'S-1-5-21-1409167834-891301383-2860967316-1145' + UserPrincipalName = '' + } - Mock -CommandName Get-ADObject -ParameterFilter { - $mockADComputer.SamAccountName -eq $Identity - } -MockWith { - Write-Verbose "Call Get-ADObject with $($mockADComputer.SamAccountName)" - return $mockADComputer - } + $mockGetTargetResourceResultsStandAlone = @{ + ServiceAccountName = $mockGetAdServiceAccountResultsStandAlone.Name + DistinguishedName = $mockGetAdServiceAccountResultsStandAlone.DistinguishedName + Path = $mockDefaultMsaPath + Description = $mockGetAdServiceAccountResultsStandAlone.Description + DisplayName = $mockGetAdServiceAccountResultsStandAlone.DisplayName + AccountType = 'Standalone' + Ensure = 'Present' + Enabled = $true + ManagedPasswordPrincipals = @() + MembershipAttribute = 'SamAccountName' + Credential = $mockCredentials + DomainController = $mockDomainController + KerberosEncryptionType = 'RC4', 'AES128', 'AES256' + } - Mock -CommandName Get-ADObject -ParameterFilter { - $mockADUSer.SamAccountName -eq $Identity - } -MockWith { - Write-Verbose "Call Get-ADObject with $($mockADUser.SamAccountName)" - return $mockADUser - } + $mockGetTargetResourceResultsGroup = @{ + ServiceAccountName = $mockGetAdServiceAccountResultsGroup.Name + DistinguishedName = $mockGetAdServiceAccountResultsGroup.DistinguishedName + Path = $mockDefaultMsaPath + Description = $mockGetAdServiceAccountResultsGroup.Description + DisplayName = $mockGetAdServiceAccountResultsGroup.DisplayName + AccountType = 'Group' + Ensure = 'Present' + Enabled = $true + ManagedPasswordPrincipals = $mockGetAdServiceAccountResultsGroup.PrincipalsAllowedToRetrieveManagedPassword + MembershipAttribute = 'SamAccountName' + Credential = $mockCredentials + DomainController = $mockDomainController + KerberosEncryptionType = 'RC4', 'AES128', 'AES256' + } - It 'Should mock call to Get-ADServiceAccount return identical information' { - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'SamAccountName' - } + $mockGetTargetResourceResultsStandAloneAbsent = @{ + ServiceAccountName = $mockGetAdServiceAccountResultsStandAlone.Name + DistinguishedName = $null + Path = $null + Description = $null + DisplayName = $null + AccountType = $null + Ensure = 'Absent' + Enabled = $false + ManagedPasswordPrincipals = @() + MembershipAttribute = 'SamAccountName' + KerberosEncryptionType = @() + } - $getTargetResourceResult = Get-TargetResource @testResourceParametersGroup - - $getTargetResourceResult.ServiceAccountName | Should -Be $mockGroupServiceAccount.Name - $getTargetResourceResult.Ensure | Should -Be 'Present' - $getTargetResourceResult.AccountType | Should -Be 'Group' - $getTargetResourceResult.Description | Should -Be $mockGroupServiceAccount.Description - $getTargetResourceResult.DisplayName | Should -Be $mockGroupServiceAccount.DisplayName - $getTargetResourceResult.Members | Should -Be ` - @($mockADUSer.($testResourceParametersGroup.MembershipAttribute), ` - $mockADComputer.($testResourceParametersGroup.MembershipAttribute)) - $getTargetResourceResult.Path | Should -Be $mockPath - } + #region Function Get-TargetResource + Describe -Name 'MSFT_ADManagedServiceAccount\Get-TargetResource' -Tag 'Get' { + BeforeAll { + Mock -CommandName Assert-Module + Mock -CommandName Get-ADObjectParentDN + Mock -CommandName Get-AdObject } - Context -Name 'When the system is NOT in the desired state (Both)' { - Mock -CommandName Get-ADServiceAccount -MockWith { - throw New-Object Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException - } - - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - } - - $getTargetResourceResult = Get-TargetResource @testResourceParametersSingle - - It "Should return 'Ensure' is 'Absent'" { - $getTargetResourceResult.Ensure | Should -Be 'Absent' - } - - It "Should return 'ServiceAccountName' when 'Absent'" { - $getTargetResourceResult.ServiceAccountName | Should -Not -BeNullOrEmpty - $getTargetResourceResult.ServiceAccountName | Should -BeExactly $testResourceParametersSingle.ServiceAccountName - } + $getTargetResourceParametersStandalone = @{ + ServiceAccountName = $mockAdServiceAccountStandAlone.ServiceAccountName + AccountType = $mockAdServiceAccountStandAlone.AccountType } - } - #endregion Function Get-TargetResource - - #region Function Compare-TargetResourceState - Describe -Name 'MSFT_ADManagedServiceAccount\Compare-TargetResourceState' -Tag 'Compare' { - Context -Name 'When the system is in the desired state (sMSA)' { - Mock -CommandName Get-TargetResource -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - Write-Verbose "Calling Get-TargetResource with $($mockSingleServiceAccount.Name)" - return $mockGetSingleServiceAccount - } - - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - AccountType = 'Single' - Path = $mockPath - Description = $mockSingleServiceAccount.Description - Ensure = 'Present' - DisplayName = $mockSingleServiceAccount.DisplayName - } - - $getTargetResourceResult = Compare-TargetResourceState @testResourceParametersSingle - $testCases = @() - $getTargetResourceResult | ForEach-Object { - $testCases += @{ - Parameter = $_.Parameter - Expected = $_.Expected - Actual = $_.Actual - Pass = $_.Pass - } - } - - It "Should return identical information for " -TestCases $testCases { - param - ( - [Parameter()] - $Parameter, - - [Parameter()] - $Expected, - - [Parameter()] - $Actual, - - [Parameter()] - $Pass - ) - - $Expected | Should -BeExactly $Actual - $Pass | Should -BeTrue - } + $getTargetResourceParametersGroup = @{ + ServiceAccountName = $mockAdServiceAccountGroup.ServiceAccountName + AccountType = $mockAdServiceAccountGroup.AccountType } - Context -Name 'When the system is in the desired state (gMSA)' { - Mock -CommandName Get-TargetResource -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $MembershipAttribute -eq 'SamAccountName' - } -MockWith { - Write-Verbose 'Group MSA using sAMAccountName' - return $mockGetGroupServiceAccount - } + Context 'When the resource is Present' { - $mockGetGroupServiceAccountDN = $mockGetGroupServiceAccount.Clone() - $mockGetGroupServiceAccountDN['MembershipAttribute'] = 'DistinguishedName' - $mockGetGroupServiceAccountDN['Members'] = @($mockADUSer.DistinguishedName, $mockADComputer.DistinguishedName) - Mock -CommandName Get-TargetResource -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $MembershipAttribute -eq 'DistinguishedName' - } -MockWith { - Write-Verbose 'Group MSA using DistinguishedName' - return $mockGetGroupServiceAccountDN - } + Context 'When the Resouce is a StandAlone account' { + Mock -CommandName Get-ADServiceAccount ` + -MockWith { $mockGetAdServiceAccountResultsStandAlone } - $mockGetGroupServiceAccountSID = $mockGetGroupServiceAccount.Clone() - $mockGetGroupServiceAccountSID['MembershipAttribute'] = 'SID' - $mockGetGroupServiceAccountSID['Members'] = @($mockADUSer.SID, $mockADComputer.SID) - Mock -CommandName Get-TargetResource -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $MembershipAttribute -eq 'SID' - } -MockWith { - Write-Verbose 'Group MSA using SID' - return $mockGetGroupServiceAccountSID - } + Mock -CommandName Get-AdObjectParentDN ` + -MockWith { $mockDefaultMsaPath } - $mockGetGroupServiceAccountOID = $mockGetGroupServiceAccount.Clone() - $mockGetGroupServiceAccountOID['MembershipAttribute'] = 'ObjectGUID' - $mockGetGroupServiceAccountOID['Members'] = @($mockADUSer.ObjectGUID, $mockADComputer.ObjectGUID) - Mock -CommandName Get-TargetResource -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $MembershipAttribute -eq 'ObjectGUID' - } -MockWith { - Write-Verbose 'Group MSA using ObjectGUID' - return $mockGetGroupServiceAccountOID - } + $result = Get-TargetResource @getTargetResourceParametersStandalone - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'SamAccountName' - AccountType = 'Group' - Path = $mockPath - Description = $mockGroupServiceAccount.Description - Ensure = 'Present' - Members = 'Node1$', 'User1' - DisplayName = $mockGroupServiceAccount.DisplayName - } - - $getTargetResourceResult = Compare-TargetResourceState @testResourceParametersGroup - $testCases = @() - $getTargetResourceResult | ForEach-Object { - $testCases += @{ - Parameter = $_.Parameter - Expected = $_.Expected - Actual = $_.Actual - Pass = $_.Pass - } - } - - It "Should return identical information for " -TestCases $testCases { - param - ( - [Parameter()] - $Parameter, - - [Parameter()] - $Expected, - - [Parameter()] - $Actual, - - [Parameter()] - $Pass - ) - - $Expected | Should -BeExactly $Actual - $Pass | Should -BeTrue - } - - It "Should return identical information for 'Members' when using 'SamAccountName'" { - $testResourceParametersGroupSAM = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'SamAccountName' - Members = 'Node1$', 'User1' - AccountType = 'Group' - } - - $getTargetResourceResultSAM = Compare-TargetResourceState @testResourceParametersGroupSAM - - $getTargetResourceResultSAM.Expected | Should -BeExactly $getTargetResourceResultSAM.Actual - $getTargetResourceResultSAM.Pass | Should -BeTrue - } - - It "Should return identical information for 'Members' when using 'DistinguishedName'" { - $testResourceParametersGroupDN = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'DistinguishedName' - Members = 'CN=Node1,OU=Fake,DC=contoso,DC=com', 'CN=User1,OU=Fake,DC=contoso,DC=com' - AccountType = 'Group' - } - - $getTargetResourceResultDN = Compare-TargetResourceState @testResourceParametersGroupDN - - $getTargetResourceResultDN.Expected | Should -BeExactly $getTargetResourceResultDN.Actual - $getTargetResourceResultDN.Pass | Should -BeTrue - } - - It "Should return identical information for 'Members' when using 'SID'" { - $testResourceParametersGroupSID = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'SID' - Members = 'S-1-5-21-1409167834-891301383-2860967316-1143', 'S-1-5-21-1409167834-891301383-2860967316-1142' - AccountType = 'Group' + foreach ($property in $mockAdServiceAccountStandalone.Keys) + { + It "Should return the correct $property property" { + $result.$property | Should -Be $mockAdServiceAccountStandalone.$property + } } - $getTargetResourceResultSID = Compare-TargetResourceState @testResourceParametersGroupSID - - $getTargetResourceResultSID.Expected | Should -BeExactly $getTargetResourceResultSID.Actual - $getTargetResourceResultSID.Pass | Should -BeTrue - } - - It "Should return identical information for 'Members' when using 'ObjectGUID'" { - $testResourceParametersGroupGUID = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'ObjectGUID' - Members = '91bffe90-4c84-4026-b1fc-d03671ff56ac', '91bffe90-4c84-4026-b1fc-d03671ff56ab' - AccountType = 'Group' + It 'Should return the correct Ensure property' { + $result.Ensure | Should -Be 'Present' } - $getTargetResourceResultGUID = Compare-TargetResourceState @testResourceParametersGroupGUID - - $getTargetResourceResultGUID.Expected | Should -BeExactly $getTargetResourceResultGUID.Actual - $getTargetResourceResultGUID.Pass | Should -BeTrue - } - } - - Context -Name 'When the system is NOT in the desired state (sMSA)' { - Mock -CommandName Get-TargetResource -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - return $mockGetSingleServiceAccount - } - - $testResourceParametersSingleNotCompliant = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - AccountType = 'Group' - Path = 'OU=FakeWrong,DC=contoso,DC=com' - Description = 'Test MSA description Wrong' - Ensure = 'Absent' - DisplayName = 'WrongDisplayName' - } - - $getTargetResourceResult = Compare-TargetResourceState @testResourceParametersSingleNotCompliant - $testCases = @() - # Need to remove parameters that will always be true - $getTargetResourceResult = $getTargetResourceResult | Where-Object -FilterScript { - $_.Parameter -ne 'ServiceAccountName' -and - $_.Parameter -ne 'DistinguishedName' -and - $_.Parameter -ne 'MembershipAttribute' - } - - $getTargetResourceResult | ForEach-Object { - $testCases += @{ - Parameter = $_.Parameter - Expected = $_.Expected - Actual = $_.Actual - Pass = $_.Pass + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ADServiceAccount ` + -ParameterFilter { $Identity -eq $getTargetResourceParametersStandalone.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-AdObject ` + -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADObjectParentDN ` + -ParameterFilter { $DN -eq $mockAdServiceAccountStandalone.DistinguishedName } ` + -Exactly -Times 1 } } - It "Should return false for " -TestCases $testCases { - param - ( - [Parameter()] - $Parameter, - - [Parameter()] - $Expected, - - [Parameter()] - $Actual, - - [Parameter()] - $Pass - ) - - $Expected | Should -Not -Be $Actual - $Pass | Should -BeFalse - } - - } - - Context -Name 'When the system is NOT in the desired state (gMSA)' { - Mock -CommandName Get-TargetResource -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $MembershipAttribute -eq 'SamAccountName' - } -MockWith { - Write-Verbose 'Group MSA using sAMAccountName' - return $mockGetGroupServiceAccount - } + Context 'When the Resouce is a Group account' { + Mock -CommandName Get-ADServiceAccount ` + -MockWith { $mockGetAdServiceAccountResultsGroup } - $mockGetGroupServiceAccountDN = $mockGetGroupServiceAccount.Clone() - $mockGetGroupServiceAccountDN['MembershipAttribute'] = 'DistinguishedName' - $mockGetGroupServiceAccountDN['Members'] = @($mockADUSer.DistinguishedName, $mockADComputer.DistinguishedName) - Mock -CommandName Get-TargetResource -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $MembershipAttribute -eq 'DistinguishedName' - } -MockWith { - Write-Verbose 'Group MSA using DistinguishedName' - return $mockGetGroupServiceAccountDN - } + Mock -CommandName Get-ADObject ` + -ParameterFilter { $mockADComputer.SamAccountName -eq $Identity } ` + -MockWith { $mockADComputer } - $mockGetGroupServiceAccountSID = $mockGetGroupServiceAccount.Clone() - $mockGetGroupServiceAccountSID['MembershipAttribute'] = 'SID' - $mockGetGroupServiceAccountSID['Members'] = @($mockADUSer.SID, $mockADComputer.SID) - Mock -CommandName Get-TargetResource -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $MembershipAttribute -eq 'SID' - } -MockWith { - Write-Verbose 'Group MSA using SID' - return $mockGetGroupServiceAccountSID - } + Mock -CommandName Get-ADObject ` + -ParameterFilter { $mockADUser.SamAccountName -eq $Identity } ` + -MockWith { $mockADUser } - $mockGetGroupServiceAccountOID = $mockGetGroupServiceAccount.Clone() - $mockGetGroupServiceAccountOID['MembershipAttribute'] = 'ObjectGUID' - $mockGetGroupServiceAccountOID['Members'] = @($mockADUSer.ObjectGUID, $mockADComputer.ObjectGUID) - Mock -CommandName Get-TargetResource -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $MembershipAttribute -eq 'ObjectGUID' - } -MockWith { - Write-Verbose 'Group MSA using ObjectGUID' - return $mockGetGroupServiceAccountOID - } + Mock -CommandName Get-AdObjectParentDN ` + -MockWith { $mockDefaultMsaPath } - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - AccountType = 'Single' - Path = 'OU=FakeWrong,DC=contoso,DC=com' - Description = 'Test MSA description Wrong' - Ensure = 'Absent' - DisplayName = 'WrongDisplayName' - MembershipAttribute = 'SamAccountName' - } + $result = Get-TargetResource @getTargetResourceParametersGroup - $getTargetResourceResult = Compare-TargetResourceState @testResourceParametersGroup - $testCases = @() - # Need to remove parameters that will always be true - $getTargetResourceResult = $getTargetResourceResult | Where-Object -FilterScript { - $_.Parameter -ne 'ServiceAccountName' -and - $_.Parameter -ne 'DistinguishedName' -and - $_.Parameter -ne 'MembershipAttribute' - } - $getTargetResourceResult | ForEach-Object { - $testCases += @{ - Parameter = $_.Parameter - Expected = $_.Expected - Actual = $_.Actual - Pass = $_.Pass + foreach ($property in $mockAdServiceAccountGroup.Keys) + { + It "Should return the correct $property property" { + $result.$property | Should -Be $mockAdServiceAccountGroup.$property + } } - } - - It "Should return false for " -TestCases $testCases { - param - ( - [Parameter()] - $Parameter, - - [Parameter()] - $Expected, - - [Parameter()] - $Actual, - [Parameter()] - $Pass - ) - - $Expected | Should -Not -Be $Actual - $Pass | Should -BeFalse - } - - It "Should return false for 'Members' when using 'SamAccountName'" { - $testResourceParametersGroupSAM = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'SamAccountName' - Members = 'Node1$' - AccountType = 'Group' + It 'Should return the correct Ensure property' { + $result.Ensure | Should -Be 'Present' } - $getTargetResourceResultSAM = Compare-TargetResourceState @testResourceParametersGroupSAM - - $membersState = $getTargetResourceResultSAM | Where-Object -FilterScript {$_.Parameter -eq 'Members'} - $membersState.Expected | Should -Not -BeExactly $membersState.Actual - $membersState.Pass | Should -BeFalse - } - - It "Should return false for 'Members' when using 'DistinguishedName'" { - $testResourceParametersGroupDN = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'DistinguishedName' - Members = 'CN=Node1,OU=Fake,DC=contoso,DC=com' - AccountType = 'Group' + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ADServiceAccount ` + -ParameterFilter { $Identity -eq $getTargetResourceParametersGroup.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-AdObject ` + -ParameterFilter { ` + $Identity -eq $mockGetAdServiceAccountResultsGroup.PrincipalsAllowedToRetrieveManagedPassword[0] } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-AdObject ` + -ParameterFilter { ` + $Identity -eq $mockGetAdServiceAccountResultsGroup.PrincipalsAllowedToRetrieveManagedPassword[1] } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ADObjectParentDN ` + -ParameterFilter { $DN -eq $mockGetAdServiceAccountResultsGroup.DistinguishedName } ` + -Exactly -Times 1 } - - $getTargetResourceResultDN = Compare-TargetResourceState @testResourceParametersGroupDN - - $membersState = $getTargetResourceResultDN | Where-Object -FilterScript {$_.Parameter -eq 'Members'} - $membersState.Expected | Should -Not -BeExactly $membersState.Actual - $membersState.Pass | Should -BeFalse } + Context 'When Get-AdServiceAccount throws an unexpected error' { + Mock -CommandName Get-ADServiceAccount ` + -MockWith { throw 'UnexpectedError' } - It "Should return false for 'Members' when using 'SID'" { - $testResourceParametersGroupSID = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'SID' - Members = 'S-1-5-21-1409167834-891301383-2860967316-1143' - AccountType = 'Group' + It 'Should throw the correct exception' { + { Get-TargetResource @getTargetResourceParametersStandAlone } | + Should -Throw ($script:localizedData.RetrievingManagedServiceAccountError -f + $getTargetResourceParametersStandAlone.ServiceAccountName) } - - $getTargetResourceResultSID = Compare-TargetResourceState @testResourceParametersGroupSID - - $membersState = $getTargetResourceResultSID | Where-Object -FilterScript {$_.Parameter -eq 'Members'} - $membersState.Expected | Should -Not -BeExactly $membersState.Actual - $membersState.Pass | Should -BeFalse } - It "Should return false for 'Members' when using 'ObjectGUID'" { - $testResourceParametersGroupGUID = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'ObjectGUID' - Members = '91bffe90-4c84-4026-b1fc-d03671ff56ac' - AccountType = 'Group' - } + Context 'When the group service account member property contains an unknown principal' { + $mockGetAdServiceAccountResultsGroupUnknownPrincipal = $mockGetAdServiceAccountResultsGroup.Clone() + $mockGetAdServiceAccountResultsGroupUnknownPrincipal.PrincipalsAllowedtoRetrieveManagedPassword = ` + $mockADUSer.ObjectSid, $mockADComputer.ObjectSid - $getTargetResourceResultGUID = Compare-TargetResourceState @testResourceParametersGroupGUID + Mock -CommandName Get-ADServiceAccount ` + -MockWith { $mockGetAdServiceAccountResultsGroupUnknownPrincipal } - $membersState = $getTargetResourceResultGUID | Where-Object -FilterScript {$_.Parameter -eq 'Members'} - $membersState.Expected | Should -Not -BeExactly $membersState.Actual - $membersState.Pass | Should -BeFalse - } - } - } - #endregion Function Compare-TargetResourceState + Mock -CommandName Get-ADObject ` + -MockWith { throw New-Object Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException } - #region Function Test-TargetResource - Describe -Name 'MSFT_ADManagedServiceAccount\Test-TargetResource' -Tag 'Test' { - Context -Name "When the system is in the desired state and 'Ensure' is 'Present' (sMSA)" { - It "Should pass when the Parameters are properly set" { - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name)" - return $mockCompareSingleServiceAccount - } + $result = Get-TargetResource @getTargetResourceParametersGroup - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - AccountType = 'Single' - Path = $mockPath - Description = $mockSingleServiceAccount.Description - Ensure = 'Present' - DisplayName = '' + It 'Should return the correct ManagedPasswordPrincipals property`' { + $result.ManagedPasswordPrincipals | Should -Be $mockADUSer.ObjectSid, $mockADComputer.ObjectSid } - - Test-TargetResource @testResourceParametersSingle | Should -BeTrue } - } - - Context -Name "When the system is in the desired state and 'Ensure' is 'Present' (gMSA)" { - It "Should pass when the Parameters are properly set" { - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockGroupServiceAccount.Name)" - return $mockCompareGroupServiceAccount - } - - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'SamAccountName' - AccountType = 'Group' - Path = $mockPath - Description = $mockGroupServiceAccount.Description - Ensure = 'Present' - Members = 'Node1$', 'User1' - DisplayName = '' - } - Test-TargetResource @testResourceParametersGroup | Should -BeTrue - } - } + Context 'When Get-AdObject throws an unexpected error' { + Mock -CommandName Get-ADServiceAccount ` + -MockWith { $mockGetAdServiceAccountResultsGroup } - Context -Name "When the system is in the desired state and 'Ensure' is 'Absent' (Both)" { - It "Should pass when 'Ensure' is set to 'Absent" { - $mockCompareSingleServiceAccountEnsureAbsent = $mockCompareSingleServiceAccount.Clone() - $objectEnsure = $mockCompareSingleServiceAccountEnsureAbsent | Where-Object -FilterScript {$_.Parameter -eq 'Ensure'} - $objectEnsure.Actual = 'Absent' - $objectEnsure.Pass = $true - - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name)" - return $mockCompareSingleServiceAccountEnsureAbsent - } + Mock -CommandName Get-ADObject ` + -MockWith { throw 'UnexpectedError' } - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - Ensure = 'Absent' + It 'Should throw the correct exception' { + { Get-TargetResource @getTargetResourceParametersGroup } | + Should -Throw ($script:localizedData.RetrievingManagedPasswordPrincipalsError -f + $mockGetAdServiceAccountResultsGroup.PrincipalsAllowedToRetrieveManagedPassword[0]) } - - Test-TargetResource @testResourceParametersSingle | Should -BeTrue } } - Context -Name "When the system is NOT in the desired state and 'Ensure' is 'Present' (sMSA)" { - $mockCompareSingleServiceAccountNotCompliant = Copy-ArrayObjects $mockCompareSingleServiceAccount + Context 'When the resource is Absent' { - $testIncorrectParameters = @{ - AccountType = 'Group' - Path = 'WrongPath' - Description = 'WrongDescription' - Ensure = 'Absent' - DisplayName = 'DisplayNameWrong' - } + Context 'When the Resouce is a StandAlone account' { + Mock -CommandName Get-AdServiceAccount ` + -MockWith { throw New-Object Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException } - $testCases = @() - foreach($incorrectParameter in $testIncorrectParameters.GetEnumerator()) - { - $objectParameter = $mockCompareSingleServiceAccountNotCompliant | Where-Object -FilterScript { $_.Parameter -eq $incorrectParameter.Name } - $objectParameter.Expected = $incorrectParameter.Value - $objectParameter.Pass = $false + $result = Get-TargetResource @getTargetResourceParametersStandalone - $testCases += @{ - Parameter = $incorrectParameter.Name - Value = $incorrectParameter.Value + foreach ($property in $mockAdServiceAccountStandaloneAbsent.Keys) + { + It "Should return the correct $property property" { + $result.$property | Should -Be $mockAdServiceAccountStandaloneAbsent.$property + } } - } - - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name)" - return $mockCompareSingleServiceAccountNotCompliant - } - It "Should return $false when is incorrect" -TestCases $testCases { - param - ( - [Parameter()] - $Parameter, - - [Parameter()] - $Value - ) - - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - AccountType = 'Single' - Path = $mockPath - Description = $mockSingleServiceAccount.Description - Ensure = 'Present' - DisplayName = '' + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ADServiceAccount ` + -ParameterFilter { ` + $Identity -eq $getTargetResourceParametersStandalone.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-AdObject ` + -Exactly -Times 0 } - - $testResourceParametersSingle[$Parameter] = $value - Test-TargetResource @testResourceParametersSingle | Should -BeFalse } - } - Context -Name "When the system is NOT in the desired state and 'Ensure' is 'Present' (gMSA)" { - $mockCompareGroupServiceAccountNotCompliant = Copy-ArrayObjects $mockCompareGroupServiceAccount + Context 'When the Resouce is a Group account' { + Mock -CommandName Get-AdServiceAccount ` + -MockWith { throw New-Object Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException } - $testIncorrectParameters = @{ - AccountType = 'Single' - Path = 'WrongPath' - Description = 'WrongDescription' - Ensure = 'Absent' - Members = '' - DisplayName = 'DisplayNameWrong' - } - - $testCases = @() - foreach($incorrectParameter in $testIncorrectParameters.GetEnumerator()) - { - $objectParameter = $mockCompareGroupServiceAccountNotCompliant | Where-Object -FilterScript { $_.Parameter -eq $incorrectParameter.Name } - $objectParameter.Expected = $incorrectParameter.Value - $objectParameter.Pass = $false + $result = Get-TargetResource @getTargetResourceParametersGroup - $testCases += @{ - Parameter = $incorrectParameter.Name - Value = $incorrectParameter.Value + foreach ($property in $mockAdServiceAccountGroupAbsent.Keys) + { + It "Should return the correct $property property" { + $result.$property | Should -Be $mockAdServiceAccountGroupAbsent.$property + } } - } - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockGroupServiceAccount.Name)" - return $mockCompareGroupServiceAccountNotCompliant - } - - It "Should return $false when is incorrect" -TestCases $testCases { - param - ( - [Parameter()] - $Parameter, - - [Parameter()] - $Value - ) - - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'SamAccountName' - AccountType = 'Group' - Path = $mockPath - Description = $mockGroupServiceAccount.Description - Ensure = 'Present' - Members = 'Node1$', 'User1' - DisplayName = '' + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ADServiceAccount ` + -ParameterFilter { $Identity -eq $getTargetResourceParametersGroup.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-AdObject ` + -Exactly -Times 0 } - - $testResourceParametersGroup[$Parameter] = $value - Test-TargetResource @testResourceParametersGroup | Should -BeFalse } } } - #endregion Function Test-TargetResource - - Describe -Name 'MSFT_ADManagedServiceAccount\New-ADServiceAccountHelper' { - BeforeAll { - Mock -CommandName New-ADServiceAccount - } - - Context -Name "When the system is NOT in the desired state and 'Ensure' is 'Present' (sMSA)" { - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - AccountType = 'Single' - Path = $mockPath - Description = $mockSingleServiceAccount.Description - Ensure = 'Present' - DisplayName = 'NewDisplayName' - } - - It 'Should call New-ADServiceAccount' { - New-ADServiceAccountHelper @testResourceParametersSingle - Assert-MockCalled -CommandName New-ADServiceAccount -Scope It -Exactly -Times 1 - } - } - - Context -Name "When the system is NOT in the desired state and 'Ensure' is 'Present' (gMSA)" { - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'SamAccountName' - AccountType = 'Group' - Path = $mockPath - Description = $mockGroupServiceAccount.Description - Ensure = 'Present' - Members = 'Node1$', 'User1' - DisplayName = '' - } - - It 'Should call New-ADServiceAccount' { - New-ADServiceAccountHelper @testResourceParametersGroup - Assert-MockCalled -CommandName New-ADServiceAccount -Scope It -Exactly -Times 1 - } - } - } - - #region Function Set-TargetResource - Describe -Name 'MSFT_ADManagedServiceAccount\Set-TargetResource' -Tag 'Set' { - BeforeAll { - Mock -CommandName New-ADServiceAccountHelper - Mock -CommandName Remove-ADServiceAccount - Mock -CommandName Move-ADObject - Mock -CommandName Set-ADServiceAccount - } - - Context -Name "When the system is in the desired state and 'Ensure' is 'Present' (sMSA)" { - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name)" - return $mockCompareSingleServiceAccount - } - - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - AccountType = 'Single' - Path = $mockPath - Description = $mockSingleServiceAccount.Description - Ensure = 'Present' - DisplayName = '' - } - - It 'Should NOT take any action when all parameters are correct' { - Set-TargetResource @testResourceParametersSingle - - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 - } - } - - Context -Name "When the system is in the desired state and 'Ensure' is 'Present' (gMSA)" { - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockGroupServiceAccount.Name)" - return $mockCompareGroupServiceAccount - } - - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - MembershipAttribute = 'SamAccountName' - AccountType = 'Group' - Path = $mockPath - Description = $mockGroupServiceAccount.Description - Ensure = 'Present' - Members = 'Node1$', 'User1' - DisplayName = '' - } + #endregion Function Get-TargetResource - It 'Should NOT take any action when all parameters are correct' { - Set-TargetResource @testResourceParametersGroup + #region Function Test-TargetResource + Describe -Name 'MSFT_ADManagedServiceAccount\Test-TargetResource' -Tag 'Test' { - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 - } + $testTargetResourceParametersStandalone = @{ + ServiceAccountName = $mockAdServiceAccountStandalone.ServiceAccountName + AccountType = $mockAdServiceAccountStandalone.AccountType + Description = $mockAdServiceAccountStandalone.Description + DisplayName = $mockAdServiceAccountStandalone.DisplayName + KerberosEncryptionType = $mockAdServiceAccountStandalone.KerberosEncryptionType + ManagedPasswordPrincipals = $mockAdServiceAccountStandalone.ManagedPasswordPrincipals + MembershipAttribute = $mockAdServiceAccountStandalone.MembershipAttribute + Ensure = $mockAdServiceAccountStandalone.Ensure } - Context -Name "When the system is in the desired state and 'Ensure' is 'Absent' (Both)" { - $mockCompareSingleServiceAccountEnsureAbsent = $mockCompareSingleServiceAccount.Clone() - $objectEnsure = $mockCompareSingleServiceAccountEnsureAbsent | Where-Object -FilterScript {$_.Parameter -eq 'Ensure'} - $objectEnsure.Actual = 'Absent' - $objectEnsure.Pass = $true - - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name)" - return $mockCompareSingleServiceAccountEnsureAbsent - } + $testTargetResourceParametersStandaloneAbsent = $testTargetResourceParametersStandalone.Clone() + $testTargetResourceParametersStandaloneAbsent.Ensure = 'Absent' - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - Ensure = 'Absent' - } + Context 'When the Resource is Present' { - It "Should pass when 'Ensure' is set to 'Absent" { - Set-TargetResource @testResourceParametersSingle + Mock -CommandName Get-TargetResource -MockWith { $mockGetTargetResourceResultsStandAlone } - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 - } - } - - Context -Name "When the system is NOT in the desired state and 'Ensure' is 'Present' (sMSA)" { - $mockCompareSingleServiceAccountNotCompliantPath = Copy-ArrayObjects $mockCompareSingleServiceAccount - $mockCompareSingleServiceAccountNotCompliantOtherParameters = Copy-ArrayObjects $mockCompareSingleServiceAccount - $mockCompareSingleServiceAccountNotCompliantAccountType = Copy-ArrayObjects $mockCompareSingleServiceAccount - $mockCompareSingleServiceAccountNotCompliantEnsure = Copy-ArrayObjects $mockCompareSingleServiceAccount - - #region Incorrect Path setup - $objectPath = $mockCompareSingleServiceAccountNotCompliantPath | Where-Object -FilterScript {$_.Parameter -eq 'Path'} - $objectPath.Expected = 'WrongPath' - $objectPath.Pass = $false - - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName -and $Path -eq $objectPath.Expected - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name) and Path '$($objectPath.Expected)'" - return $mockCompareSingleServiceAccountNotCompliantPath - } - #endregion Incorrect Path setup + Context 'When the Resource should be Present' { - It "Should call 'Move-ADObject' when 'Path' is incorrect" { - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - Path = $objectPath.Expected + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParametersStandalone } | Should -Not -Throw } - Set-TargetResource @testResourceParametersSingle - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 - } - - #region Incorrect parameter test setup - $testIncorrectParameters = @{ - Description = 'WrongDescription' - DisplayName = 'WrongDisplayName' - } - - $testCases = @() - foreach($incorrectParameter in $testIncorrectParameters.GetEnumerator()) - { - $objectParameter = $mockCompareSingleServiceAccountNotCompliantOtherParameters | - Where-Object -FilterScript { $_.Parameter -eq $incorrectParameter.Name } - $objectParameter.Expected = $incorrectParameter.Value - $objectParameter.Pass = $false - - $testCases += @{ - Parameter = $incorrectParameter.Name - Value = $incorrectParameter.Value + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { ` + $ServiceAccountName -eq $testTargetResourceParametersStandalone.ServiceAccountName } ` + -Exactly -times 1 } - } - - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName -and ( - $Description -eq $testIncorrectParameters.Description -or - $DisplayName -eq $testIncorrectParameters.DisplayName - ) - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name) and incorrect parameters" - return $mockCompareSingleServiceAccountNotCompliantOtherParameters - } - #endregion Incorrect parameter test setup - It "Should call 'Set-ADServiceAccount' when '' is incorrect" -TestCases $testCases { - param - ( - [Parameter()] - $Parameter, + Context 'When all the resource properties are in the desired state' { - [Parameter()] - $Value - ) - - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - } - $testResourceParametersSingle[$Parameter] = $Value - - Set-TargetResource @testResourceParametersSingle - - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 1 - } - - #region Incorrect Account type setup - $objectAccountType = $mockCompareSingleServiceAccountNotCompliantAccountType | Where-Object -FilterScript {$_.Parameter -eq 'AccountType'} - $objectAccountType.Expected = 'Group' - $objectAccountType.Pass = $false - - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName -and $AccountType -eq $objectAccountType.Expected - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name) and AccountType '$($objectAccountType.Expected)'" - return $mockCompareSingleServiceAccountNotCompliantAccountType - } - #endregion Incorrect Account type setup - - It "Should NOT call 'Remove-ADServiceAccount, New-ADServiceAccountHelper' when 'AccountType' is incorrect and 'AccountTypeForce' is false" { - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - AccountType = $objectAccountType.Expected - AccountTypeForce = $false + It 'Should return $true' { + Test-TargetResource @testTargetResourceParametersStandalone | Should -Be $true + } } - # Check if Warning is returned - Set-TargetResource @testResourceParametersSingle 3>&1 | Should -Not -Be $null + foreach ($property in $mockAdServiceAccountChanged.Keys) + { + Context "When the $property resource property is not in the desired state" { - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 - } + It 'Should return $false' { + $testTargetResourceParametersChanged = $testTargetResourceParametersStandalone.Clone() + $testTargetResourceParametersChanged.$property = $mockAdServiceAccountChanged.$property - It "Should call 'Remove-ADServiceAccount, New-ADServiceAccountHelper' when 'AccountType' is incorrect and 'AccountTypeForce' is true" { - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - AccountType = 'Group' - AccountTypeForce = $true + Test-TargetResource @testTargetResourceParametersChanged | Should -Be $false + } + } } - - Set-TargetResource @testResourceParametersSingle - - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 1 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 } - #region Incorrect Ensure setup - $objectEnsure = $mockCompareSingleServiceAccountNotCompliantEnsure | Where-Object -FilterScript {$_.Parameter -eq 'Ensure'} - $objectEnsure.Expected = 'Absent' - $objectEnsure.Pass = $false + Context 'When the Resource should be Absent' { - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName -and $Ensure -eq $objectEnsure.Expected - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name) and Ensure '$($objectEnsure.Expected)'" - return $mockCompareSingleServiceAccountNotCompliantEnsure - } - #endregion Incorrect Ensure type setup - - It "Should call 'Remove-ADServiceAccount' when 'Ensure' is set to 'Absent'" { - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - Ensure = $objectEnsure.Expected + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParametersStandAloneAbsent } | Should -Not -Throw } - Set-TargetResource @testResourceParametersSingle - - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 - } - - } - - Context -Name "When the system is NOT in the desired state and 'Ensure' is 'Present' (gMSA)" { - $mockCompareGroupServiceAccountNotCompliantPath = Copy-ArrayObjects $mockCompareGroupServiceAccount - $mockCompareGroupServiceAccountNotCompliantOtherParameters = Copy-ArrayObjects $mockCompareGroupServiceAccount - $mockCompareGroupServiceAccountNotCompliantAccountType = Copy-ArrayObjects $mockCompareGroupServiceAccount - $mockCompareGroupServiceAccountNotCompliantEnsure = Copy-ArrayObjects $mockCompareGroupServiceAccount - - #region Incorrect Path setup - $objectPath = $mockCompareGroupServiceAccountNotCompliantPath | Where-Object -FilterScript {$_.Parameter -eq 'Path'} - $objectPath.Expected = 'WrongPath' - $objectPath.Pass = $false - - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $Path -eq $objectPath.Expected - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockGroupServiceAccount.Name) and Path '$($objectPath.Expected)'" - return $mockCompareGroupServiceAccountNotCompliantPath - } - #endregion Incorrect Path setup - - It "Should call 'Move-ADObject' when 'Path' is incorrect" { - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - AccountType = 'Group' - Path = $objectPath.Expected + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { + $ServiceAccountName -eq $testTargetResourceParametersStandAloneAbsent.ServiceAccountName } ` + -Exactly -times 1 } - Set-TargetResource @testResourceParametersGroup - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 - } - - #region Incorrect parameter test setup - $testIncorrectParameters = @{ - Description = 'WrongDescription' - DisplayName = 'WrongDisplayName' - Members = 'WrongUser' - } - - $testCases = @() - foreach($incorrectParameter in $testIncorrectParameters.GetEnumerator()) - { - $objectParameter = $mockCompareGroupServiceAccountNotCompliantOtherParameters | - Where-Object -FilterScript { $_.Parameter -eq $incorrectParameter.Name } - $objectParameter.Expected = $incorrectParameter.Value - $objectParameter.Pass = $false - - $testCases += @{ - Parameter = $incorrectParameter.Name - Value = $incorrectParameter.Value + It 'Should return $false' { + Test-TargetResource @testTargetResourceParametersStandAloneAbsent | Should -Be $false } } + } - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and ( - $Description -eq $testIncorrectParameters.Description -or - $DisplayName -eq $testIncorrectParameters.DisplayName -or - $Members -eq $testIncorrectParameters.Members - ) - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockGroupServiceAccount.Name) and incorrect parameters" - return $mockCompareGroupServiceAccountNotCompliantOtherParameters - } - #endregion Incorrect parameter test setup + Context 'When the Resource is Absent' { - It "Should call 'Set-ADServiceAccount' when '' is incorrect" -TestCases $testCases { - param - ( - [Parameter()] - $Parameter, + Mock -CommandName Get-TargetResource -MockWith { $mockGetTargetResourceResultsStandAloneAbsent } - [Parameter()] - $Value - ) + Context 'When the Resource should be Present' { - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - AccountType = 'Group' + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParametersStandAlone } | Should -Not -Throw } - $testResourceParametersGroup[$Parameter] = $Value - - Set-TargetResource @testResourceParametersGroup - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 1 - } - - #region Incorrect Account type setup - $objectAccountType = $mockCompareGroupServiceAccountNotCompliantAccountType | Where-Object -FilterScript {$_.Parameter -eq 'AccountType'} - $objectAccountType.Expected = 'Single' - $objectAccountType.Pass = $false - - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $AccountType -eq $objectAccountType.Expected - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockGroupServiceAccount.Name) and AccountType '$($objectAccountType.Expected)'" - return $mockCompareGroupServiceAccountNotCompliantAccountType - } - #endregion Incorrect Account type setup - It "Should NOT call 'Remove-ADServiceAccount, New-ADServiceAccountHelper' when 'AccountType' is incorrect and 'AccountTypeForce' is false" { - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - AccountType = $objectAccountType.Expected - AccountTypeForce = $false + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { + $ServiceAccountName -eq $testTargetResourceParametersStandAlone.ServiceAccountName } ` + -Exactly -times 1 } - # Check if Warning is returned - Set-TargetResource @testResourceParametersGroup 3>&1 | Should -Not -Be $null - - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 - } - - It "Should call 'Remove-ADServiceAccount, New-ADServiceAccountHelper' when 'AccountType' is incorrect and 'AccountTypeForce' is true" { - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - AccountType = 'Single' - AccountTypeForce = $true + It 'Should return $false' { + Test-TargetResource @testTargetResourceParametersStandAlone | Should -Be $false } - - Set-TargetResource @testResourceParametersGroup - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 1 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 } - #region Incorrect Ensure setup - $objectEnsure = $mockCompareGroupServiceAccountNotCompliantEnsure | Where-Object -FilterScript {$_.Parameter -eq 'Ensure'} - $objectEnsure.Expected = 'Absent' - $objectEnsure.Pass = $false + Context 'When the Resource should be Absent' { - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockGroupServiceAccount.Name -eq $ServiceAccountName -and $Ensure -eq $objectEnsure.Expected - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockGroupServiceAccount.Name) and Ensure '$($objectEnsure.Expected)'" - return $mockCompareGroupServiceAccountNotCompliantEnsure - } - #endregion Incorrect Ensure type setup - - It "Should call 'Remove-ADServiceAccount' when 'Ensure' is set to 'Absent'" { - $testResourceParametersGroup = @{ - ServiceAccountName = $mockGroupServiceAccount.Name - AccountType = 'Group' - Ensure = $objectEnsure.Expected + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParametersStandAloneAbsent } | Should -Not -Throw } - Set-TargetResource @testResourceParametersGroup - - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 0 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 - } - } - - Context -Name "When the system is NOT in the desired state and 'Ensure' is 'Present' (Both)" { - $mockCompareSingleServiceAccountNotEnsure = Copy-ArrayObjects $mockCompareSingleServiceAccount - - #region Incorrect Ensure setup - $objectEnsure = $mockCompareSingleServiceAccountNotEnsure | Where-Object -FilterScript {$_.Parameter -eq 'Ensure'} - $objectEnsure.Expected = 'Present' - $objectEnsure.Actual = 'Absent' - $objectEnsure.Pass = $false - - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name)" - return $mockCompareSingleServiceAccountNotEnsure - } - #endregion Incorrect Ensure setup - - It "Should call 'New-AdServiceAccount' when 'Ensure' is set to 'Present" { - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - Ensure = $objectEnsure.Expected + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { ` + $ServiceAccountName -eq $testTargetResourceParametersStandAloneAbsent.ServiceAccountName } ` + -Exactly -times 1 } - Set-TargetResource @testResourceParametersSingle - - Assert-MockCalled -CommandName Compare-TargetResourceState -Scope It -Times 1 - Assert-MockCalled -CommandName New-ADServiceAccountHelper -Scope It -Times 1 - Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 - Assert-MockCalled -CommandName Set-ADServiceAccount -Scope It -Exactly -Times 0 + It 'Should return $true' { + Test-TargetResource @testTargetResourceParametersStandAloneAbsent | Should -Be $true + } } } + } + #endregion Function Test-TargetResource - Context 'When system cannot connect to domain or other errors' { - Mock -CommandName Move-ADObject -MockWith { - Write-Verbose "Calling 'Move-ADObject' and throwing an error" - throw 'Microsoft.ActiveDirectory.Management.ADServerDownException' - } - - $mockCompareSingleServiceAccountNotCompliantPath = Copy-ArrayObjects $mockCompareSingleServiceAccount - - #region Incorrect Path setup - $objectPath = $mockCompareSingleServiceAccountNotCompliantPath | Where-Object -FilterScript {$_.Parameter -eq 'Path'} - $objectPath.Expected = 'WrongPath' - $objectPath.Pass = $false - - Mock -CommandName Compare-TargetResourceState -ParameterFilter { - $mockSingleServiceAccount.Name -eq $ServiceAccountName -and $Path -eq $objectPath.Expected - } -MockWith { - Write-Verbose "Calling Compare-TargetResourceState with $($mockSingleServiceAccount.Name) and Path '$($objectPath.Expected)'" - return $mockCompareSingleServiceAccountNotCompliantPath + #region Function Set-TargetResource + Describe -Name 'MSFT_ADManagedServiceAccount\Set-TargetResource' -Tag 'Set' { + BeforeAll { + $mockGetAdDomainResults = @{ + DistinguishedName = 'DC=' + $mockDomainName.Replace('.', ',DC=') } - #endregion Incorrect Path setup - It 'Should call "Move-ADObject" and throw an error when catching any other errors besides "Account Not Found"'{ - $testResourceParametersSingle = @{ - ServiceAccountName = $mockSingleServiceAccount.Name - Path = $objectPath.Expected + Mock -CommandName New-ADServiceAccount + Mock -CommandName Remove-ADServiceAccount + Mock -CommandName Move-ADObject + Mock -CommandName Set-ADServiceAccount + Mock -CommandName Get-DomainName -MockWith { $mockDomainName } + Mock -CommandName Get-ADDomain -MockWith { $mockGetAdDomainResults } + } + + $setTargetResourceParametersStandAlone = @{ + ServiceAccountName = $mockAdServiceAccountStandAlone.ServiceAccountName + AccountType = $mockAdServiceAccountStandAlone.AccountType + Path = $mockDefaultMsaPath + Description = $mockAdServiceAccountStandalone.Description + Ensure = $mockAdServiceAccountStandAlone.Ensure + DisplayName = $mockAdServiceAccountStandAlone.DisplayName + KerberosEncryptionType = $mockAdServiceAccountStandAlone.KerberosEncryptionType + } + + $setTargetResourceParametersStandAloneAbsent = $setTargetResourceParametersStandAlone.Clone() + $setTargetResourceParametersStandAloneAbsent.Ensure = 'Absent' + + $setTargetResourceParametersGroup = @{ + ServiceAccountName = $mockAdServiceAccountGroup.ServiceAccountName + MembershipAttribute = $mockAdServiceAccountGroup.MembershipAttribute + AccountType = $mockAdServiceAccountGroup.AccountType + Path = $mockDefaultMsaPath + Description = $mockAdServiceAccountGroup.Description + Ensure = $mockAdServiceAccountGroup.Ensure + ManagedPasswordPrincipals = $mockAdServiceAccountGroup.ManagedPasswordPrincipals + DisplayName = $mockAdServiceAccountGroup.Name.DisplayName + KerberosEncryptionType = $mockAdServiceAccountGroup.KerberosEncryptionType + } + Context 'When the Resource should be Present' { + + Mock -CommandName Get-TargetResource -MockWith { $mockGetTargetResourceResultsStandAlone } + + Context 'When the Resource is Present' { + + foreach ($property in $mockAdServiceAccountChanged.Keys) + { + $setTargetResourceParametersChangedProperty = $setTargetResourceParametersGroup.Clone() + $setTargetResourceParametersChangedProperty.$property = $mockAdServiceAccountChanged.$property + + Mock -CommandName Get-TargetResource ` + -ParameterFilter { $mockGetAdServiceAccountResultsGroup.Name -eq $ServiceAccountName } ` + -MockWith { $mockGetTargetResourceResultsGroup } + + It "Should call the correct mocks when $property has changed" { + Set-TargetResource @setTargetResourceParametersChangedProperty + + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { ` + $ServiceAccountName -eq $setTargetResourceParametersChangedProperty.ServiceAccountName } ` + -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName New-ADServiceAccount -Scope It -Exactly -Times 0 + Assert-MockCalled -CommandName Remove-ADServiceAccount -Scope It -Exactly -Times 0 + Assert-MockCalled -CommandName Move-ADObject -Scope It -Exactly -Times 0 + Assert-MockCalled -CommandName Set-ADServiceAccount ` + -ParameterFilter { ` + $Identity -eq $setTargetResourceParametersChangedProperty.ServiceAccountName } ` + -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DomainName -Scope It -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADDomain -Scope It -Exactly -Times 0 + } + } + + Context 'When ''Set-AdServiceAccount'' throws an exception' { + Mock -CommandName Set-ADServiceAccount -MockWith { throw 'UnexpectedError' } + + It 'Should throw the correct exception' { + { Set-TargetResource @setTargetResourceParametersChangedProperty } | + Should -Throw ($script:localizedData.SettingManagedServiceAccountError -f + $setTargetResourceParametersChangedProperty.AccountType, + $setTargetResourceParametersChangedProperty.ServiceAccountName) + } + } + + Context 'When the Resource has a changed AccountType' { + $setTargetResourceParametersChangedAccountType = $setTargetResourceParametersStandAlone.Clone() + $setTargetResourceParametersChangedAccountType.AccountType = 'Group' + + Mock -CommandName Get-TargetResource -MockWith { $mockGetTargetResourceResultsStandAlone } + + It 'Should call the correct mocks' { + Set-TargetResource @setTargetResourceParametersChangedAccountType + + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { ` + $ServiceAccountName -eq $setTargetResourceParametersChangedAccountType.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName New-ADServiceAccount ` + -ParameterFilter { ` + $Name -eq $setTargetResourceParametersChangedAccountType.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Remove-ADServiceAccount ` + -ParameterFilter { ` + $Identity -eq $setTargetResourceParametersChangedAccountType.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DomainName -Exactly -Times 1 + Assert-MockCalled -CommandName Move-ADObject -Exactly -Times 0 + Assert-MockCalled -CommandName Set-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADDomain -Exactly -Times 0 + } + + Context 'When ''Remove-AdServiceAccount'' throws an exception' { + Mock -CommandName Remove-ADServiceAccount -MockWith { throw 'UnexpectedError' } + + It 'Should throw the correct exception' { + { Set-TargetResource @setTargetResourceParametersChangedAccountType } | + Should -Throw ($script:localizedData.RemovingManagedServiceAccountError -f + $setTargetResourceParametersChangedAccountType.AccountType, + $setTargetResourceParametersChangedAccountType.ServiceAccountName) + } + } + } + + Context 'When the Resource has a changed Path' { + $setTargetResourceParametersChangedPath = $setTargetResourceParametersStandAlone.Clone() + $setTargetResourceParametersChangedPath.Path = $mockChangedPath + + Mock -CommandName Get-TargetResource -MockWith { $mockGetTargetResourceResultsStandAlone } + + It 'Should call the correct mocks' { + Set-TargetResource @setTargetResourceParametersChangedPath + + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { $ServiceAccountName -eq $setTargetResourceParametersChangedPath.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName New-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Remove-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Move-ADObject ` + -ParameterFilter { $Identity -eq $mockGetTargetResourceResultsStandAlone.DistinguishedName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Set-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Get-DomainName -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADDomain -Exactly -Times 0 + } + + Context 'When ''Move-AdObject'' throws an exception' { + Mock -CommandName Move-AdObject -MockWith { throw 'UnexpectedError' } + + It 'Should throw the correct exception' { + { Set-TargetResource @setTargetResourceParametersChangedPath } | + Should -Throw ($script:localizedData.MovingManagedServiceAccountError -f + $setTargetResourceParametersChangedPath.AccountType, + $setTargetResourceParametersChangedPath.ServiceAccountName, + $mockGetTargetResourceResultsStandAlone.Path, + $setTargetResourceParametersChangedPath.Path) + } + } + } + } + + Context 'When the Resource is Absent' { + Mock -CommandName Get-TargetResource -MockWith { $mockGetTargetResourceResultsStandAloneAbsent } + + Context 'When the resource is a Standalone Account' { + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParametersStandAlone } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { ` + $ServiceAccountName -eq $setTargetResourceParametersStandAlone.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName New-ADServiceAccount ` + -ParameterFilter { $Name -eq $setTargetResourceParametersStandAlone.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Remove-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Move-ADObject -Exactly -Times 0 + Assert-MockCalled -CommandName Set-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Get-DomainName -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADDomain -Exactly -Times 0 + } + + Context 'When "New-AdServiceAccount" throws an unexpected exception' { + Mock -CommandName New-AdServiceAccount -MockWith { throw 'UnexpectedError' } + + It 'Should throw the correct exception' { + { Set-TargetResource @setTargetResourceParametersStandAlone } | + Should -Throw ($script:localizedData.AddingManagedServiceAccountError -f + $setTargetResourceParametersStandAlone.AccountType, + $setTargetResourceParametersStandAlone.ServiceAccountName, + $setTargetResourceParametersStandAlone.Path) + } + + Context 'When the Path property has not been specified' { + $setTargetResourceParametersStandAloneNoPath = $setTargetResourceParametersStandAlone.Clone() + $setTargetResourceParametersStandAloneNoPath.Remove('Path') + + It 'Should throw the correct exception' { + { Set-TargetResource @setTargetResourceParametersStandAloneNoPath } | + Should -Throw ($script:localizedData.AddingManagedServiceAccountError -f + $setTargetResourceParametersStandAloneNoPath.AccountType, + $setTargetResourceParametersStandAloneNoPath.ServiceAccountName, + $mockDefaultMsaPath) + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-ADDomain -Scope Context -Exactly -Times 1 + } + + Context 'when "Get-ADDomain" throws an exception' { + Mock -CommandName Get-ADDomain -MockWith { throw 'UnexpectedError' } + + It 'Should throw the correct exception' { + { Set-TargetResource @setTargetResourceParametersStandAloneNoPath } | + Should -Throw $script:localizedData.GettingADDomainError + } + } + } + } + } + + Context 'When the resource is a Group Account' { + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParametersGroup } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { + $ServiceAccountName -eq $setTargetResourceParametersGroup.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName New-ADServiceAccount ` + -ParameterFilter { $Name -eq $setTargetResourceParametersGroup.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DomainName -Exactly -Times 1 + Assert-MockCalled -CommandName Remove-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Move-ADObject -Exactly -Times 0 + Assert-MockCalled -CommandName Set-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADDomain -Exactly -Times 0 + } + + Context 'When "New-AdServiceAccount" throws an "ADException KDS key not found" exception' { + $mockADException = [Microsoft.ActiveDirectory.Management.ADException]::new() + $mockADException.ErrorCode = $script:errorCodeKdsRootKeyNotFound + + Mock -CommandName New-AdServiceAccount -MockWith { throw $mockADException } + + It 'Should throw the correct exception' { + { Set-TargetResource @setTargetResourceParametersGroup } | + Should -Throw ($script:localizedData.KdsRootKeyNotFoundError -f + $setTargetResourceParametersGroup.ServiceAccountName) + } + } + + Context 'When "New-AdServiceAccount" throws an unknown "ADException" exception' { + $mockADException = [Microsoft.ActiveDirectory.Management.ADException]::new() + + Mock -CommandName New-AdServiceAccount -MockWith { throw $mockADException } + + It 'Should throw the correct exception' { + { Set-TargetResource @setTargetResourceParametersGroup } | + Should -Throw ($script:localizedData.AddingManagedServiceAccountError -f + $setTargetResourceParametersGroup.AccountType, + $setTargetResourceParametersGroup.ServiceAccountName, + $setTargetResourceParametersGroup.Path) + } + } + } + } + } + + Context 'When the Resource should be Absent' { + + Context 'When the Resource is Present' { + Mock -CommandName Get-TargetResource -MockWith { $mockGetTargetResourceResultsStandAlone } + + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParametersStandAloneAbsent } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { + $ServiceAccountName -eq $setTargetResourceParametersStandAloneAbsent.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName New-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Remove-ADServiceAccount ` + -ParameterFilter { + $Identity -eq $setTargetResourceParametersStandAloneAbsent.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Move-ADObject -Exactly -Times 0 + Assert-MockCalled -CommandName Set-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Get-DomainName -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADDomain -Exactly -Times 0 + } + + Context 'When ''Remove-AdServiceAccount'' throws an exception' { + Mock -CommandName Remove-ADServiceAccount -MockWith { throw 'UnexpectedError' } + + It 'Should throw the correct exception' { + { Set-TargetResource @setTargetResourceParametersStandAloneAbsent } | + Should -Throw ($script:localizedData.RemovingManagedServiceAccountError -f + $setTargetResourceParametersStandAloneAbsent.AccountType, + $setTargetResourceParametersStandAloneAbsent.ServiceAccountName) + } + } + } + + Context 'When the Resource is Absent' { + Mock -CommandName Get-TargetResource -MockWith { $mockGetTargetResourceResultsStandAloneAbsent } + + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParametersStandAloneAbsent } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Get-TargetResource ` + -ParameterFilter { ` + $ServiceAccountName -eq $setTargetResourceParametersStandAloneAbsent.ServiceAccountName } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName New-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Remove-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Move-ADObject -Exactly -Times 0 + Assert-MockCalled -CommandName Set-ADServiceAccount -Exactly -Times 0 + Assert-MockCalled -CommandName Get-DomainName -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADDomain -Exactly -Times 0 } - { Set-TargetResource @testResourceParametersSingle -ErrorAction 'SilentlyContinue' } | - Should -Throw ($script:localizedData.AddingManagedServiceAccountError -f $testResourceParametersSingle.ServiceAccountName) } } }