diff --git a/CHANGELOG.md b/CHANGELOG.md index f5bed41..758dcf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Carbon.DSC Changelog +## 1.1.0 + +* Added support to Carbon_Group resource for adding new members without removing any members. +* Added support to Carbon_Group resource for removing group members. + ## 1.0.1 Reducing directory depth of internal, private nested module dependencies. diff --git a/Carbon.DSC/Carbon.DSC.psd1 b/Carbon.DSC/Carbon.DSC.psd1 index 7056a89..1e255ac 100644 --- a/Carbon.DSC/Carbon.DSC.psd1 +++ b/Carbon.DSC/Carbon.DSC.psd1 @@ -18,7 +18,7 @@ RootModule = 'Carbon.DSC.psm1' # Version number of this module. - ModuleVersion = '1.0.1' + ModuleVersion = '1.1.0' # ID used to uniquely identify this module GUID = '8aa43ac0-cd66-4fc0-aa13-efa08d573946' diff --git a/Carbon.DSC/DscResources/Carbon_Group/Carbon_Group.psm1 b/Carbon.DSC/DscResources/Carbon_Group/Carbon_Group.psm1 index 3f96532..160d697 100644 --- a/Carbon.DSC/DscResources/Carbon_Group/Carbon_Group.psm1 +++ b/Carbon.DSC/DscResources/Carbon_Group/Carbon_Group.psm1 @@ -38,8 +38,9 @@ function Get-TargetResource if( $group ) { $description = $group.Description - $members = $group.Members + $members = @($group.Members | Resolve-PrincipalName) $ensure = 'Present' + $group.Dispose() } @{ @@ -57,11 +58,11 @@ function Set-TargetResource DSC resource for configuring local Windows groups. .DESCRIPTION - The `Carbon_Group` resource installs and uninstalls groups. It also adds members to existing groups. + The `Carbon_Group` resource installs and uninstalls groups. It can also modify members of existing groups. - The group is installed when `Ensure` is set to `Present`. Members of the group are updated to match the `Members` property (i.e. members not listed in the `Members` property are removed from the group). If `Members` has no value, all members are removed. Because DSC resources run under the LCM which runs as `System`, local system accounts must have access to the directories where both new and existing member accounts can be found. + The group is installed when `Ensure` is set to `Present`. Group's members are updated based on the values of both `Members` and `EnsureMembers` properties, where `Members` lists users and `EnsureMemebers` controls how the list is used. If `EnsureMembers` is set to `Exact`, the group is configured to have the exact members specified. If set to `Present`, the specified members are added to the group if they are not members already. If set to `Absent`, the specified members are removed from the group if they are members of it. Defaults to `Exact`. Because DSC resources run under the LCM which runs as `System`, local system accounts must have access to the directories where both new and existing member accounts can be found. - The group is removed when `Ensure` is set to `Absent`. When removing a group, the `Members` property is ignored. + The group is removed when `Ensure` is set to `Absent`. When removing a group, all other properties are ignored. The `Carbon_Group` resource was added in Carbon 2.1.0. @@ -82,7 +83,7 @@ function Set-TargetResource .EXAMPLE > - Demonstrates how to install a group and add members to it. + Demonstrates how to install a group and set its members. Carbon_Group 'CreateFirstOrder' { @@ -102,6 +103,30 @@ function Set-TargetResource Ensure = 'Absent'; } + .EXAMPLE + > + Demonstrates how to add members to an existing group. + + Carbon_Group 'AddVader' + { + Name = 'SithOrder'; + Ensure = 'Present'; + EnsureMembers = 'Present'; + Members = @( 'SO\DarthVader' ); + } + + .EXAMPLE + > + Demonstrates how to remove members from an existing group. + + Carbon_Group 'RemoveAnakin' + { + Name = 'JediOrder'; + Ensure = 'Present'; + EnsureMembers = 'Absent'; + Members = @( 'JO\ASkywalker' ); + } + #> [CmdletBinding(SupportsShouldProcess)] param @@ -120,10 +145,12 @@ function Set-TargetResource # Should be either `Present` or `Absent`. If set to `Present`, a group is configured and membership configured. If set to `Absent`, the group is removed. $Ensure, + # Controls how the list of users, given in the `Members` property, gets used. Should be either `Exact`, `Present` or `Absent`. If set to `Exact`, the group is configured to have the exact members specified. If set to `Present`, the specified members are added to the group if they are not members already. If set to `Absent`, the specified members are removed from the group if they are members of it. + [ValidateSet('Exact','Present','Absent')] + [String] $EnsureMembers = 'Exact', + [string[]] - # The group's members. Only used when adding/updating a group (i.e. when `Ensure` is `Present`). - # - # Members not in this list are removed from the group. + # A list of users, which is used according to value of `EnsureMembers`. Only used when adding/updating a group (i.e. when `Ensure` is `Present`). $Members = @() ) @@ -135,7 +162,19 @@ function Set-TargetResource return } - $group = Install-CGroup -Name $Name -Description $Description -Member $Members -PassThru + $memberNames = @() + if ($Members) + { + $memberNames = $Members | Resolve-MemberName + } + $currentMemberNames = (Get-TargetResource -Name $Name).Members + + $membershipChanges = Resolve-MembershipChange -EnsureMembers $EnsureMembers ` + -MemberNames $memberNames ` + -CurrentMemberNames $currentMemberNames + $membersToInstall = @($currentMemberNames;$membershipChanges.ToAdd) + + $group = Install-CGroup -Name $Name -Description $Description -Member $membersToInstall -PassThru if( -not $group ) { return @@ -143,14 +182,9 @@ function Set-TargetResource try { - $memberNames = @() - if( $Members ) - { - $memberNames = $Members | Resolve-MemberName - } $membersToRemove = $group.Members | Where-Object { $memberName = Resolve-PrincipalName -Principal $_ - return $memberNames -notcontains $memberName + return $membershipChanges.ToRemove -contains $memberName } if( $membersToRemove ) { @@ -189,6 +223,9 @@ function Test-TargetResource [String] $Ensure = "Present", + [ValidateSet('Exact','Present','Absent')] + [String] $EnsureMembers = 'Exact', + [string[]] $Members = @() ) @@ -226,35 +263,80 @@ function Test-TargetResource } $memberNames = @() - if( $Members ) + if ($Members) { - $memberNames = $Members | Resolve-MemberName + $memberNames = $Members | Resolve-MemberName } - $currentMemberNames = $resource['Members'] | Resolve-PrincipalName - # Is the current group missing the desired members? - foreach( $memberName in $memberNames ) + $membershipChanges = Resolve-MembershipChange -EnsureMembers $EnsureMembers ` + -MemberNames $memberNames ` + -CurrentMemberNames $resource.Members + + if ($membershipChanges.ToAdd) { - if( $currentMemberNames -notcontains $memberName ) + $upToDate = $false + foreach ($memberName in $membershipChanges.ToAdd) { Write-Verbose -Message ('[{0}] [Members] {1} is absent but should be present' -f $Name,$memberName) - $upToDate = $false } } - - # Does the current group contains extra members? - foreach( $memberName in $currentMemberNames ) + if ($membershipChanges.ToRemove) { - if( $memberNames -notcontains $memberName ) + $upToDate = $false + foreach ($memberName in $membershipChanges.ToRemove) { Write-Verbose -Message ('[{0}] [Members] {1} is present but should be absent' -f $Name,$memberName) - $upToDate = $false } } - return $upToDate } +function Resolve-MembershipChange +{ + [CmdletBinding()] + [OutputType([hashtable])] + param ( + [Parameter(Mandatory)] + [ValidateSet('Exact','Present','Absent')] + [String] $EnsureMembers, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [String[]] $MemberNames, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [String[]] $CurrentMemberNames + ) + + $alreadyMembers = @($MemberNames | Where-Object { $_ -in $CurrentMemberNames }) + $extraMembers = @($CurrentMemberNames | Where-Object { $_ -notin $MemberNames }) + $newMembers = @($MemberNames | Where-Object { $_ -notin $CurrentMemberNames }) + + $membersToAdd = $null + $membersToRemove = $null + if ($EnsureMembers -eq 'Present') + { + $membersToAdd = $newMembers + $membersToRemove = @() + } + elseif ($EnsureMembers -eq 'Absent') + { + $membersToAdd = @() + $membersToRemove = $alreadyMembers + } + else + { + $membersToAdd = $newMembers + $membersToRemove = $extraMembers + } + + return @{ + ToAdd = $membersToAdd + ToRemove = $membersToRemove + } +} + function Resolve-MemberName { param( diff --git a/Carbon.DSC/DscResources/Carbon_Group/Carbon_Group.schema.mof b/Carbon.DSC/DscResources/Carbon_Group/Carbon_Group.schema.mof index 7a480d2..a9b2283 100644 --- a/Carbon.DSC/DscResources/Carbon_Group/Carbon_Group.schema.mof +++ b/Carbon.DSC/DscResources/Carbon_Group/Carbon_Group.schema.mof @@ -18,6 +18,7 @@ class Carbon_Group : OMI_BaseResource [Key, Description("Name of the group to configure.")] String Name; [Write, Description("Description of the group.")] String Description; [Write, Description("Determines whether the group is created or deleted"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; - [Write, Description("Members of the group. After configuration, only these members will be in the group.")] String Members[]; + [Write, Description("Controls how the list of users, given in the `Members` property, get used"), ValueMap{"Exact", "Present","Absent"}, Values{"Exact", "Present","Absent"}] String EnsureMembers; + [Write, Description("A list of users, which is used according to value of `EnsureMembers`")] String Members[]; }; diff --git a/Tests/Carbon_Group.Tests.ps1 b/Tests/Carbon_Group.Tests.ps1 index 021f7ec..55a6646 100644 --- a/Tests/Carbon_Group.Tests.ps1 +++ b/Tests/Carbon_Group.Tests.ps1 @@ -57,20 +57,9 @@ Describe 'Carbon_Group' { Assert-DscResourcePresent $resource $resource.Members.Count | Should -Be $admins.Members.Count + $resourceMembers = $resource.Members | ForEach-Object { Resolve-CIdentity -Name $_ } - foreach( $admin in $admins.Members ) - { - $found = $false - foreach( $potentialAdmin in $resource.Members ) - { - if( $potentialAdmin.Sid -eq $admin.Sid ) - { - $found = $true - break - } - } - $found | Should -BeTrue - } + $resourceMembers.Sid | Should -Be $admins.Members.Sid } It 'get target resource does not exist' { @@ -126,6 +115,62 @@ Describe 'Carbon_Group' { $result = Test-TargetResource -Name $script:groupName -Description $script:description -Ensure Absent $result | Should -Not -BeNullOrEmpty $result | Should -BeFalse + + # EnsureMembers='Present' + $commonSetupArgs = @{ + Name = $script:groupName; + Ensure = 'Present'; + Description = $script:description; + EnsureMembers = 'Present'; + } + + $result = Test-TargetResource -Members @() @commonSetupArgs + $result | Should -Not -BeNullOrEmpty + $result | Should -BeTrue -Because 'the specified members list is empty' + + $result = Test-TargetResource -Members $script:username1,$script:username2 @commonSetupArgs + $result | Should -Not -BeNullOrEmpty + $result | Should -BeTrue -Because 'the specified members list matches the current group members list' + + $result = Test-TargetResource -Members $script:username2 @commonSetupArgs + $result | Should -Not -BeNullOrEmpty + $result | Should -BeTrue -Because 'all specified members exist in the current group members list' + + $result = Test-TargetResource -Members $script:username3 @commonSetupArgs + $result | Should -Not -BeNullOrEmpty + $result | Should -BeFalse -Because 'all specified members do not exist in the current group members list' + + $result = Test-TargetResource -Members $script:username2,$script:username3 @commonSetupArgs + $result | Should -Not -BeNullOrEmpty + $result | Should -BeFalse -Because 'only some specified members exist in the current group members list' + + # EnsureMembers='Absent' + $commonSetupArgs = @{ + Name = $script:groupName; + Ensure = 'Present'; + Description = $script:description; + EnsureMembers = 'Absent'; + } + + $result = Test-TargetResource -Members @() @commonSetupArgs + $result | Should -Not -BeNullOrEmpty + $result | Should -BeTrue -Because 'the specified members array is empty' + + $result = Test-TargetResource -Members $script:username3 @commonSetupArgs + $result | Should -Not -BeNullOrEmpty + $result | Should -BeTrue -Because 'all specified members do not exist in the current group members list' + + $result = Test-TargetResource -Members $script:username1,$script:username2 @commonSetupArgs + $result | Should -Not -BeNullOrEmpty + $result | Should -BeFalse -Because 'the specified members list matches the current group members list' + + $result = Test-TargetResource -Members $script:username2 @commonSetupArgs + $result | Should -Not -BeNullOrEmpty + $result | Should -BeFalse -Because 'all specified members exist in the current group members list' + + $result = Test-TargetResource -Members $script:username2,$script:username3 @commonSetupArgs + $result | Should -Not -BeNullOrEmpty + $result | Should -BeFalse -Because 'only some specified members exist in the current group members list' } It 'set target resource' { @@ -143,13 +188,46 @@ Describe 'Carbon_Group' { $group.Members.Count | Should -Be 0 # Change members - Set-TargetResource -Name $script:groupName -Members $script:username1 -Ensure 'Present' + ## EnsureMembers='Exact' + foreach ($i in 0..1) + { + Set-TargetResource -Name $script:groupName -Members $script:username1 -Ensure 'Present' + } + $group = Get-CGroup -Name $script:groupName $group | Should -Not -BeNullOrEmpty $group.Name | Should -Be $script:groupName $group.Description | Should -BeNullOrEmpty $group.Members.Count | Should -Be 1 - $group.Members[0].Sid | Should -Be $script:user1.Sid + $group.Members.Sid | Should -Be $script:user1.Sid + + ## EnsureMembers='Present' + foreach ($i in 0..1) + { + Set-TargetResource -Name $script:groupName -EnsureMembers 'Present' -Members $script:username2 -Ensure 'Present' + } + + $group = Get-CGroup -Name $script:groupName + $group | Should -Not -BeNullOrEmpty + $group.Name | Should -Be $script:groupName + $group.Description | Should -BeNullOrEmpty + $group.Members.Count | Should -Be 2 + ($group.Members.Sid -contains $script:user1.Sid) | Should -BeTrue -Because "$script:username1 is an existing member" + ($group.Members.Sid -contains $script:user2.Sid) | Should -BeTrue -Because "$script:username2 should have been added to the group" + + ## EnsureMembers='Absent' + foreach ($i in 0..1) + { + Set-TargetResource -Name $script:groupName -EnsureMembers 'Absent' -Members $script:username2 -Ensure 'Present' + } + + $group = Get-CGroup -Name $script:groupName + $group | Should -Not -BeNullOrEmpty + $group.Name | Should -Be $script:groupName + $group.Description | Should -BeNullOrEmpty + $group.Members.Count | Should -Be 1 + ($group.Members.Sid -notcontains $script:user2.Sid) | Should -BeTrue -Because "$script:username2 should have been removed to the group" + ($group.Members.Sid -contains $script:user1.Sid) | Should -BeTrue -Because "$script:username1 is an existing member" # Change description Set-TargetResource -Name $script:groupName -Members $script:username1 -Description 'group description' -Ensure 'Present'