summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Saraceni <andrew.saraceni@gmail.com>2017-07-31 14:10:57 -0400
committerMatt Davis <nitzmahone@users.noreply.github.com>2017-07-31 11:10:57 -0700
commit7b3d893f2de0771c70fddc2e3662bd018fe13e58 (patch)
tree0ed77913683d55b5f1fdbee0b613d6588f7cadee
parenta01884ca2fb915399043482c37c60a734b3cccb1 (diff)
downloadansible-7b3d893f2de0771c70fddc2e3662bd018fe13e58.tar.gz
New Module: Manage Windows local group membership (win_group_member) (#26307)
* initial commit for win_group_member module * fix variable name change for split_adspath * correct ordering of examples/return data to match documentation verbiage * change tests setup/teardown to use new group rather than an inbult group
-rw-r--r--lib/ansible/modules/windows/win_group_member.ps1230
-rw-r--r--lib/ansible/modules/windows/win_group_member.py101
-rw-r--r--test/integration/targets/win_group_member/aliases1
-rw-r--r--test/integration/targets/win_group_member/tasks/main.yml31
-rw-r--r--test/integration/targets/win_group_member/tasks/tests.yml258
5 files changed, 621 insertions, 0 deletions
diff --git a/lib/ansible/modules/windows/win_group_member.ps1 b/lib/ansible/modules/windows/win_group_member.ps1
new file mode 100644
index 0000000000..52624a31d5
--- /dev/null
+++ b/lib/ansible/modules/windows/win_group_member.ps1
@@ -0,0 +1,230 @@
+#!powershell
+
+# (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# WANT_JSON
+# POWERSHELL_COMMON
+
+$ErrorActionPreference = "Stop"
+
+function Test-GroupMember {
+ <#
+ .SYNOPSIS
+ Parse desired member into domain and username.
+ Also, ensure member can be resolved/exists on the target system by checking its SID.
+ .NOTES
+ Returns a hashtable of the same type as returned from Get-GroupMember.
+ Accepts username (users, groups) and domains in the following formats:
+ - username
+ - .\username
+ - SERVERNAME\username
+ - NT AUTHORITY\username
+ - DOMAIN\username
+ - username@DOMAIN
+ #>
+ param(
+ [String]$GroupMember
+ )
+
+ $parsed_member = @{
+ domain = $null
+ username = $null
+ combined = $null
+ }
+
+ # Split domain and account name into separate values
+ # '\' or '@' needs additional parsing, otherwise assume local computer
+
+ if ($GroupMember -match "\\") {
+ # DOMAIN\username
+ $split_member = $GroupMember.Split("\")
+
+ if ($split_member[0] -in @($env:COMPUTERNAME, ".")) {
+ # Local
+ $parsed_member.domain = $env:COMPUTERNAME
+ }
+ else {
+ # Domain or service (i.e. NT AUTHORITY)
+ $parsed_member.domain = $split_member[0]
+ }
+ $parsed_member.username = $split_member[1]
+ }
+ elseif ($GroupMember -match "@") {
+ # username@DOMAIN
+ $parsed_member.domain = $GroupMember.Split("@")[1]
+ $parsed_member.username = $GroupMember.Split("@")[0]
+ }
+ else {
+ # Local
+ $parsed_member.domain = $env:COMPUTERNAME
+ $parsed_member.username = $GroupMember
+ }
+
+ if ($parsed_member.domain -match "\.") {
+ # Assume FQDN was passed - change to NetBIOS/short name for later ADSI membership comparisons
+ $netbios_name = (Get-CimInstance -ClassName Win32_NTDomain -Filter "DnsForestName = '$($parsed_member.domain)'").DomainName
+
+ if (!$netbios_name) {
+ Fail-Json -obj $result -message "Could not resolve NetBIOS name for domain $($parsed_member.domain)"
+ }
+ $parsed_member.domain = $netbios_name
+ }
+
+ # Set SID check arguments, and 'combined' for later comparison and output reporting
+ if ($parsed_member.domain -eq $env:COMPUTERNAME) {
+ $sid_check_args = @($parsed_member.username)
+ $parsed_member.combined = "{0}" -f $parsed_member.username
+ }
+ else {
+ $sid_check_args = @($parsed_member.domain, $parsed_member.username)
+ $parsed_member.combined = "{0}\{1}" -f $parsed_member.domain, $parsed_member.username
+ }
+
+ try {
+ $user_object = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $sid_check_args
+ $user_object.Translate([System.Security.Principal.SecurityIdentifier])
+ }
+ catch {
+ Fail-Json -obj $result -message "Could not resolve group member $GroupMember"
+ }
+
+ return $parsed_member
+}
+
+function Get-GroupMember {
+ <#
+ .SYNOPSIS
+ Retrieve group members for a given group, and return in a common format.
+ .NOTES
+ Returns an array of hashtables of the same type as returned from Test-GroupMember.
+ #>
+ param(
+ [System.DirectoryServices.DirectoryEntry]$Group
+ )
+
+ $members = @()
+
+ $current_members = $Group.psbase.Invoke("Members") | ForEach-Object {
+ ([ADSI]$_).InvokeGet("ADsPath")
+ }
+
+ foreach ($current_member in $current_members) {
+ $parsed_member = @{
+ domain = $null
+ username = $null
+ combined = $null
+ }
+
+ $rootless_adspath = $current_member.Replace("WinNT://", "")
+ $split_adspath = $rootless_adspath.Split("/")
+
+ if ($split_adspath -match $env:COMPUTERNAME) {
+ # Local
+ $parsed_member.domain = $env:COMPUTERNAME
+ $parsed_member.username = $split_adspath[-1]
+ $parsed_member.combined = $split_adspath[-1]
+ }
+ elseif ($split_adspath.Count -eq 1 -and $split_adspath[0] -like "S-1*") {
+ # Broken SID
+ $parsed_member.username = $split_adspath[0]
+ $parsed_member.combined = $split_adspath[0]
+ }
+ else {
+ # Domain or service (i.e. NT AUTHORITY)
+ $parsed_member.domain = $split_adspath[0]
+ $parsed_member.username = $split_adspath[1]
+ $parsed_member.combined = "{0}\{1}" -f $split_adspath[0], $split_adspath[1]
+ }
+
+ $members += $parsed_member
+ }
+
+ return $members
+}
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$members = Get-AnsibleParam -obj $params -name "members" -type "list" -failifempty $true
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent"
+
+$result = @{
+ changed = $false
+ name = $name
+}
+if ($state -eq "present") {
+ $result.added = @()
+}
+elseif ($state -eq "absent") {
+ $result.removed = @()
+}
+
+$adsi = [ADSI]"WinNT://$env:COMPUTERNAME"
+$group = $adsi.Children | Where-Object { $_.SchemaClassName -eq "group" -and $_.Name -eq $name }
+
+if (!$group) {
+ Fail-Json -obj $result -message "Could not find local group $name"
+}
+
+$current_members = Get-GroupMember -Group $group
+
+foreach ($member in $members) {
+ $group_member = Test-GroupMember -GroupMember $member
+
+ $user_in_group = $false
+ foreach ($current_member in $current_members) {
+ if ($current_member.combined -eq $group_member.combined) {
+ $user_in_group = $true
+ break
+ }
+ }
+
+ $member_adspath = "WinNT://{0}/{1}" -f $group_member.domain, $group_member.username
+
+ try {
+ if ($state -eq "present" -and !$user_in_group) {
+ if (!$check_mode) {
+ $group.Add($member_adspath)
+ $result.added += $group_member.combined
+ }
+ $result.changed = $true
+ }
+ elseif ($state -eq "absent" -and $user_in_group) {
+ if (!$check_mode) {
+ $group.Remove($member_adspath)
+ $result.removed += $group_member.combined
+ }
+ $result.changed = $true
+ }
+ }
+ catch {
+ Fail-Json -obj $result -message $_.Exception.Message
+ }
+}
+
+$final_members = Get-GroupMember -Group $group
+
+if ($final_members) {
+ $result.members = [Array]$final_members.combined
+}
+else {
+ $result.members = @()
+}
+
+Exit-Json -obj $result
diff --git a/lib/ansible/modules/windows/win_group_member.py b/lib/ansible/modules/windows/win_group_member.py
new file mode 100644
index 0000000000..ac1a581f7b
--- /dev/null
+++ b/lib/ansible/modules/windows/win_group_member.py
@@ -0,0 +1,101 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.0',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_group_member
+version_added: "2.4"
+short_description: Manage Windows local group membership
+description:
+ - Allows the addition and removal of local, service and domain users,
+ and domain groups from a local group.
+options:
+ name:
+ description:
+ - Name of the local group to manage membership on.
+ required: true
+ members:
+ description:
+ - A list of members to ensure are present/absent from the group.
+ - Accepts local users as username, .\username, and SERVERNAME\username.
+ - Accepts domain users and groups as DOMAIN\username and username@DOMAIN.
+ - Accepts service users as NT AUTHORITY\username.
+ required: true
+ state:
+ description:
+ - Desired state of the members in the group.
+ choices:
+ - present
+ - absent
+ default: present
+author:
+ - Andrew Saraceni (@andrewsaraceni)
+'''
+
+EXAMPLES = r'''
+- name: Add a local and domain user to a local group
+ win_group_member:
+ name: Remote Desktop Users
+ members:
+ - NewLocalAdmin
+ - DOMAIN\TestUser
+ state: present
+
+- name: Remove a domain group and service user from a local group
+ win_group_member:
+ name: Backup Operators
+ members:
+ - DOMAIN\TestGroup
+ - NT AUTHORITY\SYSTEM
+ state: absent
+'''
+
+RETURN = r'''
+name:
+ description: The name of the target local group.
+ returned: always
+ type: string
+ sample: Administrators
+added:
+ description: A list of members added when C(state) is C(present); this is
+ empty if no members are added.
+ returned: success and C(state) is C(present)
+ type: list
+ sample: ["NewLocalAdmin", "DOMAIN\\TestUser"]
+removed:
+ description: A list of members removed when C(state) is C(absent); this is
+ empty if no members are removed.
+ returned: success and C(state) is C(absent)
+ type: list
+ sample: ["DOMAIN\\TestGroup", "NT AUTHORITY\\SYSTEM"]
+members:
+ description: A list of all local group members at completion; this is empty
+ if the group contains no members.
+ returned: success
+ type: list
+ sample: ["DOMAIN\\TestUser", "NewLocalAdmin"]
+'''
diff --git a/test/integration/targets/win_group_member/aliases b/test/integration/targets/win_group_member/aliases
new file mode 100644
index 0000000000..c6d6198167
--- /dev/null
+++ b/test/integration/targets/win_group_member/aliases
@@ -0,0 +1 @@
+windows/ci/group3
diff --git a/test/integration/targets/win_group_member/tasks/main.yml b/test/integration/targets/win_group_member/tasks/main.yml
new file mode 100644
index 0000000000..09902eb68e
--- /dev/null
+++ b/test/integration/targets/win_group_member/tasks/main.yml
@@ -0,0 +1,31 @@
+- name: Gather facts
+ setup:
+
+- name: Remove potentially leftover test group
+ win_group: &wg_absent
+ name: WinGroupMemberTest
+ state: absent
+
+- name: Add new test group
+ win_group:
+ name: WinGroupMemberTest
+ state: present
+
+- name: Run tests for win_group_member
+ block:
+
+ - name: Test in normal mode
+ include_tasks: tests.yml
+ vars:
+ win_local_group: WinGroupMemberTest
+ in_check_mode: no
+
+ - name: Test in check-mode
+ include_tasks: tests.yml
+ vars:
+ win_local_group: WinGroupMemberTest
+ in_check_mode: yes
+ check_mode: yes
+
+- name: Remove test group
+ win_group: *wg_absent
diff --git a/test/integration/targets/win_group_member/tasks/tests.yml b/test/integration/targets/win_group_member/tasks/tests.yml
new file mode 100644
index 0000000000..bdcbbf7a4a
--- /dev/null
+++ b/test/integration/targets/win_group_member/tasks/tests.yml
@@ -0,0 +1,258 @@
+# Test code for win_group_member
+
+# (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: Remove potentially leftover group members
+ win_group_member:
+ name: "{{ win_local_group }}"
+ members:
+ - Administrator
+ - Guest
+ - NT AUTHORITY\SYSTEM
+ - NT AUTHORITY\NETWORK SERVICE
+ state: absent
+
+
+- name: Add user to fake group
+ win_group_member:
+ name: FakeGroup
+ members:
+ - Administrator
+ state: present
+ register: add_user_to_fake_group
+ failed_when: add_user_to_fake_group.changed != false or add_user_to_fake_group.msg != "Could not find local group FakeGroup"
+
+
+- name: Add fake local user
+ win_group_member:
+ name: "{{ win_local_group }}"
+ members:
+ - FakeUser
+ state: present
+ register: add_fake_local_user
+ failed_when: add_fake_local_user.changed != false or add_fake_local_user.msg != "Could not resolve group member FakeUser"
+
+
+- name: Add fake FQDN domain user
+ win_group_member:
+ name: "{{ win_local_group }}"
+ members:
+ - FakeUser@domain.fake
+ state: present
+ register: add_fake_fqdn_domain_user
+ failed_when: add_fake_fqdn_domain_user.changed != false or add_fake_fqdn_domain_user.msg != "Could not resolve NetBIOS name for domain domain.fake"
+
+
+- name: Add users to group
+ win_group_member: &wgm_present
+ name: "{{ win_local_group }}"
+ members:
+ - Administrator
+ - Guest
+ - NT AUTHORITY\SYSTEM
+ state: present
+ register: add_users_to_group
+
+- name: Test add_users_to_group (normal mode)
+ assert:
+ that:
+ - add_users_to_group.changed == true
+ - add_users_to_group.added == ["Administrator", "Guest", "NT AUTHORITY\\SYSTEM"]
+ - add_users_to_group.members == ["Administrator", "Guest", "NT AUTHORITY\\SYSTEM"]
+ when: not in_check_mode
+
+- name: Test add_users_to_group (check-mode)
+ assert:
+ that:
+ - add_users_to_group.changed == true
+ - add_users_to_group.added == []
+ - add_users_to_group.members == []
+ when: in_check_mode
+
+
+- name: Add users to group (again)
+ win_group_member: *wgm_present
+ register: add_users_to_group_again
+
+- name: Test add_users_to_group_again (normal mode)
+ assert:
+ that:
+ - add_users_to_group_again.changed == false
+ - add_users_to_group_again.added == []
+ - add_users_to_group_again.members == ["Administrator", "Guest", "NT AUTHORITY\\SYSTEM"]
+ when: not in_check_mode
+
+
+- name: Add different syntax users to group (again)
+ win_group_member:
+ <<: *wgm_present
+ members:
+ - "{{ ansible_hostname }}\\Administrator"
+ - .\Guest
+ register: add_different_syntax_users_to_group_again
+
+- name: Test add_different_syntax_users_to_group_again (normal mode)
+ assert:
+ that:
+ - add_different_syntax_users_to_group_again.changed == false
+ - add_different_syntax_users_to_group_again.added == []
+ - add_different_syntax_users_to_group_again.members == ["Administrator", "Guest", "NT AUTHORITY\\SYSTEM"]
+ when: not in_check_mode
+
+- name: Test add_different_syntax_users_to_group_again (check-mode)
+ assert:
+ that:
+ - add_different_syntax_users_to_group_again.changed == true
+ - add_different_syntax_users_to_group_again.added == []
+ - add_different_syntax_users_to_group_again.members == []
+ when: in_check_mode
+
+
+- name: Add another user to group
+ win_group_member: &wgma_present
+ <<: *wgm_present
+ members:
+ - NT AUTHORITY\NETWORK SERVICE
+ register: add_another_user_to_group
+
+- name: Test add_another_user_to_group (normal mode)
+ assert:
+ that:
+ - add_another_user_to_group.changed == true
+ - add_another_user_to_group.added == ["NT AUTHORITY\\NETWORK SERVICE"]
+ - add_another_user_to_group.members == ["Administrator", "Guest", "NT AUTHORITY\\SYSTEM", "NT AUTHORITY\\NETWORK SERVICE"]
+ when: not in_check_mode
+
+- name: Test add_another_user_to_group (check-mode)
+ assert:
+ that:
+ - add_another_user_to_group.changed == true
+ - add_another_user_to_group.added == []
+ - add_another_user_to_group.members == []
+ when: in_check_mode
+
+
+- name: Add another user to group (again)
+ win_group_member: *wgma_present
+ register: add_another_user_to_group_again
+
+- name: Test add_another_user_to_group_1_again (normal mode)
+ assert:
+ that:
+ - add_another_user_to_group_again.changed == false
+ - add_another_user_to_group_again.added == []
+ - add_another_user_to_group_again.members == ["Administrator", "Guest", "NT AUTHORITY\\SYSTEM", "NT AUTHORITY\\NETWORK SERVICE"]
+ when: not in_check_mode
+
+
+- name: Remove users from group
+ win_group_member: &wgm_absent
+ <<: *wgm_present
+ state: absent
+ register: remove_users_from_group
+
+- name: Test remove_users_from_group (normal mode)
+ assert:
+ that:
+ - remove_users_from_group.changed == true
+ - remove_users_from_group.removed == ["Administrator", "Guest", "NT AUTHORITY\\SYSTEM"]
+ - remove_users_from_group.members == ["NT AUTHORITY\\NETWORK SERVICE"]
+ when: not in_check_mode
+
+- name: Test remove_users_from_group (check-mode)
+ assert:
+ that:
+ - remove_users_from_group.changed == false
+ - remove_users_from_group.removed == []
+ - remove_users_from_group.members == []
+ when: in_check_mode
+
+
+- name: Remove users from group (again)
+ win_group_member: *wgm_absent
+ register: remove_users_from_group_again
+
+- name: Test remove_users_from_group_again (normal mode)
+ assert:
+ that:
+ - remove_users_from_group_again.changed == false
+ - remove_users_from_group_again.removed == []
+ - remove_users_from_group_again.members == ["NT AUTHORITY\\NETWORK SERVICE"]
+ when: not in_check_mode
+
+
+- name: Remove different syntax users from group (again)
+ win_group_member:
+ <<: *wgm_absent
+ members:
+ - "{{ ansible_hostname }}\\Administrator"
+ - .\Guest
+ register: remove_different_syntax_users_from_group_again
+
+- name: Test remove_different_syntax_users_from_group_again (normal mode)
+ assert:
+ that:
+ - remove_different_syntax_users_from_group_again.changed == false
+ - remove_different_syntax_users_from_group_again.removed == []
+ - remove_different_syntax_users_from_group_again.members == ["NT AUTHORITY\\NETWORK SERVICE"]
+ when: not in_check_mode
+
+- name: Test add_different_syntax_users_to_group_again (check-mode)
+ assert:
+ that:
+ - remove_different_syntax_users_from_group_again.changed == false
+ - remove_different_syntax_users_from_group_again.removed == []
+ - remove_different_syntax_users_from_group_again.members == []
+ when: in_check_mode
+
+
+- name: Remove another user from group
+ win_group_member: &wgma_absent
+ <<: *wgm_absent
+ members:
+ - NT AUTHORITY\NETWORK SERVICE
+ register: remove_another_user_from_group
+
+- name: Test remove_another_user_from_group (normal mode)
+ assert:
+ that:
+ - remove_another_user_from_group.changed == true
+ - remove_another_user_from_group.removed == ["NT AUTHORITY\\NETWORK SERVICE"]
+ - remove_another_user_from_group.members == []
+ when: not in_check_mode
+
+- name: Test remove_another_user_from_group (check-mode)
+ assert:
+ that:
+ - remove_another_user_from_group.changed == false
+ - remove_another_user_from_group.removed == []
+ - remove_another_user_from_group.members == []
+ when: in_check_mode
+
+
+- name: Remove another user from group (again)
+ win_group_member: *wgma_absent
+ register: remove_another_user_from_group_again
+
+- name: Test remove_another_user_from_group_again (normal mode)
+ assert:
+ that:
+ - remove_another_user_from_group_again.changed == false
+ - remove_another_user_from_group_again.removed == []
+ - remove_another_user_from_group_again.members == []
+ when: not in_check_mode