From 22fb4c858ae854a03948f80eaa52389c078cf10d Mon Sep 17 00:00:00 2001 From: Marius Rieder Date: Fri, 15 Mar 2019 01:43:08 +0100 Subject: [PATCH] Win domain group membership module (#52556) * Add new win_domain_group_membership module. * Add support for diff mode. * Do not assign variable which is never used. * Add documentation for the `domain_*` options. * Let ansible handle the exceptions. The test if the group exists is useless as the first action on the groups fails with the same error message if it does not exist. * Add comments why we need the try/catch * Rework diff handling. Just return before/after state and let ansible do the working out of the diff. * Minor cleanups according to PR * Switch from Get-AdUser/Group to Get-AdObject so we can add/remove service accounts, or computers too. * Cleanup PowerShell code --- .../windows/win_domain_group_membership.ps1 | 124 ++++++++++++++++++ .../windows/win_domain_group_membership.py | 117 +++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 lib/ansible/modules/windows/win_domain_group_membership.ps1 create mode 100644 lib/ansible/modules/windows/win_domain_group_membership.py diff --git a/lib/ansible/modules/windows/win_domain_group_membership.ps1 b/lib/ansible/modules/windows/win_domain_group_membership.ps1 new file mode 100644 index 0000000000..7b8564364a --- /dev/null +++ b/lib/ansible/modules/windows/win_domain_group_membership.ps1 @@ -0,0 +1,124 @@ +#!powershell + +# Copyright: (c) 2019, Marius Rieder +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +try { + Import-Module ActiveDirectory +} +catch { + Fail-Json -obj @{} -message "win_domain_group_membership requires the ActiveDirectory PS module to be installed" +} + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false + +# Module control parameters +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent","pure" +$domain_username = Get-AnsibleParam -obj $params -name "domain_username" -type "str" +$domain_password = Get-AnsibleParam -obj $params -name "domain_password" -type "str" -failifempty ($null -ne $domain_username) +$domain_server = Get-AnsibleParam -obj $params -name "domain_server" -type "str" + +# Group Membership parameters +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$members = Get-AnsibleParam -obj $params -name "members" -type "list" -failifempty $true + +# Filter ADObjects by ObjectClass +$ad_object_class_filter = "(ObjectClass -eq 'user' -or ObjectClass -eq 'group' -or ObjectClass -eq 'computer' -or ObjectClass -eq 'msDS-ManagedServiceAccount')" + +$extra_args = @{} +if ($null -ne $domain_username) { + $domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $domain_password + $extra_args.Credential = $credential +} +if ($nul -ne $domain_server) { + $extra_args.Server = $domain_server +} + +$result = @{ + changed = $false + added = [System.Collections.Generic.List`1[String]]@() + removed = [System.Collections.Generic.List`1[String]]@() +} +if ($diff_mode) { + $result.diff = @{} +} + +$members_before = Get-AdGroupMember -Identity $name @extra_args +$pure_members = [System.Collections.Generic.List`1[String]]@() + +foreach ($member in $members) { + $group_member = Get-ADObject -Filter "SamAccountName -eq '$member' -and $ad_object_class_filter" -Properties objectSid, sAMAccountName + if (!$group_member) { + Fail-Json -obj $result "Could not find domain user, group, service account or computer named $member" + } + + if ($state -eq "pure") { + $pure_members.Add($group_member.objectSid) + } + + $user_in_group = $false + foreach ($current_member in $members_before) { + if ($current_member.sid -eq $group_member.objectSid) { + $user_in_group = $true + break + } + } + + if ($state -in @("present", "pure") -and !$user_in_group) { + Add-ADGroupMember -Identity $name -Members $group_member -WhatIf:$check_mode @extra_args + $result.added.Add($group_member.SamAccountName) + $result.changed = $true + } elseif ($state -eq "absent" -and $user_in_group) { + Remove-ADGroupMember -Identity $name -Members $group_member -WhatIf:$check_mode @extra_args -Confirm:$False + $result.removed.Add($group_member.SamAccountName) + $result.changed = $true + } +} + +if ($state -eq "pure") { + # Perform removals for existing group members not defined in $members + $current_members = Get-AdGroupMember -Identity $name @extra_args + + foreach ($current_member in $current_members) { + $user_to_remove = $true + foreach ($pure_member in $pure_members) { + if ($pure_member -eq $current_member.sid) { + $user_to_remove = $false + break + } + } + + if ($user_to_remove) { + Remove-ADGroupMember -Identity $name -Members $current_member -WhatIf:$check_mode @extra_args -Confirm:$False + $result.removed.Add($current_member.SamAccountName) + $result.changed = $true + } + } +} + +$final_members = Get-AdGroupMember -Identity $name @extra_args + +if ($final_members) { + $result.members = [Array]$final_members.SamAccountName +} else { + $result.members = @() +} + +if ($diff_mode -and $result.changed) { + $result.diff.before = $members_before.SamAccountName | Out-String + if (!$check_mode) { + $result.diff.after = [Array]$final_members.SamAccountName | Out-String + } else { + $after = [System.Collections.Generic.List`1[String]]$result.members + $result.removed | ForEach-Object { $after.Remove($_) > $null } + $after.AddRange($result.added) + $result.diff.after = $after | Out-String + } +} + +Exit-Json -obj $result diff --git a/lib/ansible/modules/windows/win_domain_group_membership.py b/lib/ansible/modules/windows/win_domain_group_membership.py new file mode 100644 index 0000000000..49e4fd4f3b --- /dev/null +++ b/lib/ansible/modules/windows/win_domain_group_membership.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Andrew Saraceni +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_domain_group_membership +version_added: "2.8" +short_description: Manage Windows domain group membership +description: + - Allows the addition and removal of domain users + and domain groups from/to a domain group. +options: + name: + description: + - Name of the domain group to manage membership on. + type: str + required: yes + members: + description: + - A list of members to ensure are present/absent from the group. + - The given names must be a SmaAccountName of a user, group, service account, or computer + type: list + required: yes + state: + description: + - Desired state of the members in the group. + - When C(state) is C(pure), only the members specified will exist, + and all other existing members not specified are removed. + type: str + choices: [ absent, present, pure ] + default: present + domain_username: + description: + - The username to use when interacting with AD. + - If this is not set then the user Ansible used to log in with will be + used instead when using CredSSP or Kerberos with credential delegation. + type: str + domain_password: + description: + - The password for I(username). + type: str + domain_server: + description: + - Specifies the Active Directory Domain Services instance to connect to. + - Can be in the form of an FQDN or NetBIOS name. + - If not specified then the value is based on the domain of the computer + running PowerShell. + type: str +seealso: +- module: win_domain_user +- module: win_domain_group +author: + - Marius Rieder (@jiuka) +''' + +EXAMPLES = r''' +- name: Add a domain user/group to a domain group + win_domain_group_membership: + name: Foo + members: + - Bar + state: present + +- name: Remove a domain user/group from a domain group + win_domain_group_membership: + name: Foo + members: + - Bar + state: absent + +- name: Ensure only a domain user/group exists in a domain group + win_domain_group_membership: + name: Foo + members: + - Bar + state: pure + +- name: Add a computer to a domain group + win_domain_group_membership: + name: Foo + members: + - DESKTOP$ + state: present +''' + +RETURN = r''' +name: + description: The name of the target domain group. + returned: always + type: str + sample: Domain-Admins +added: + description: A list of members added when C(state) is C(present) or + C(pure); this is empty if no members are added. + returned: success and C(state) is C(present) or C(pure) + type: list + sample: ["UserName", "GroupName"] +removed: + description: A list of members removed when C(state) is C(absent) or + C(pure); this is empty if no members are removed. + returned: success and C(state) is C(absent) or C(pure) + type: list + sample: ["UserName", "GroupName"] +members: + description: A list of all domain group members at completion; this is empty + if the group contains no members. + returned: success + type: list + sample: ["UserName", "GroupName"] +'''