From 22533c0932fc7a54ea6e5c6ed7f742f91eb377f1 Mon Sep 17 00:00:00 2001 From: Nick Chandler Date: Thu, 10 Aug 2017 20:23:09 -0400 Subject: [PATCH] New module: Add module or managing Windows Active Directory users (windows/win_domain_user) (#24075) * Initial win_domain_user module support * Add return information * Update return values * Add try/catch for PS module import * Improve win_domain_user module * Fix bad merge * Fix pep8 failure * Actually fix pep8 failure * Update win_domain_user.py to meet standards * Add check_mode support for win_domain_user * Updated documentation before merge --- .../modules/windows/win_domain_user.ps1 | 280 ++++++++++++++++ .../modules/windows/win_domain_user.py | 310 ++++++++++++++++++ 2 files changed, 590 insertions(+) create mode 100644 lib/ansible/modules/windows/win_domain_user.ps1 create mode 100644 lib/ansible/modules/windows/win_domain_user.py diff --git a/lib/ansible/modules/windows/win_domain_user.ps1 b/lib/ansible/modules/windows/win_domain_user.ps1 new file mode 100644 index 0000000000..8dac046edc --- /dev/null +++ b/lib/ansible/modules/windows/win_domain_user.ps1 @@ -0,0 +1,280 @@ +#!powershell +# 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 . + +# WANT_JSON +# POWERSHELL_COMMON + +######## +try { + Import-Module ActiveDirectory + } + catch { + Fail-Json $result "Failed to import ActiveDirectory PowerShell module. This module should be run on a domain controller, and the ActiveDirectory module must be available." + } + +$result = @{ + changed = $false + password_updated = $false +} + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false + +# Module control parameters +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent","query" +$update_password = Get-AnsibleParam -obj $params -name "update_password" -type "str" -default "always" -validateset "always","on_create" +$groups_action = Get-AnsibleParam -obj $params -name "groups_action" -type "str" -default "replace" -validateset "add","remove","replace" + +# User account parameters +$username = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$description = Get-AnsibleParam -obj $params -name "description" -type "str" +$password = Get-AnsibleParam -obj $params -name "password" -type "str" +$password_expired = Get-AnsibleParam -obj $params -name "password_expired" -type "bool" +$password_never_expires = Get-AnsibleParam -obj $params -name "password_never_expires" -type "bool" +$user_cannot_change_password = Get-AnsibleParam -obj $params -name "user_cannot_change_password" -type "bool" +$account_locked = Get-AnsibleParam -obj $params -name "account_locked" -type "bool" +$groups = Get-AnsibleParam -obj $params -name "groups" -type "list" +$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool" -default $true +$path = Get-AnsibleParam -obj $params -name "path" -type "str" +$upn = Get-AnsibleParam -obj $params -name "upn" -type "str" + +# User informational parameters +$user_info = @{ + GivenName = Get-AnsibleParam -obj $params -name "firstname" -type "str" + Surname = Get-AnsibleParam -obj $params -name "surname" -type "str" + Company = Get-AnsibleParam -obj $params -name "company" -type "str" + EmailAddress = Get-AnsibleParam -obj $params -name "email" -type "str" + StreetAddress = Get-AnsibleParam -obj $params -name "street" -type "str" + City = Get-AnsibleParam -obj $params -name "city" -type "str" + State = Get-AnsibleParam -obj $params -name "state_province" -type "str" + PostalCode = Get-AnsibleParam -obj $params -name "postal_code" -type "str" + Country = Get-AnsibleParam -obj $params -name "country" -type "str" +} + +# Parameter validation +If ($account_locked -ne $null -and $account_locked) { + Fail-Json $result "account_locked must be set to 'no' if provided" +} +If (($password_expired -ne $null) -and ($password_never_expires -ne $null)) { + Fail-Json $result "password_expired and password_never_expires are mutually exclusive but have both been set" +} + +try { + $user_obj = Get-ADUser -Identity $username -Properties * +} +catch { + $user_obj = $null +} + +If ($state -eq 'present') { + # Ensure user exists + try { + $new_user = $false + + # If the account does not exist, create it + If (-not $user_obj) { + If ($path -ne $null){ + New-ADUser -Name $username -Path $path -WhatIf:$check_mode + } + Else { + New-ADUser -Name $username -WhatIf:$check_mode + } + $new_user = $true + $result.changed = $true + If ($check_mode) { + Exit-Json $result + } + $user_obj = Get-ADUser -Identity $username -Properties * + } + + # Set the password if required + If ($password -and (($new_user -and $update_password -eq "on_create") -or $update_password -eq "always")) { + $secure_password = ConvertTo-SecureString $password -AsPlainText -Force + Set-ADAccountPassword -Identity $username -Reset:$true -Confirm:$false -NewPassword $secure_password -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.password_updated = $true + $result.changed = $true + } + + # Configure password policies + If (($password_never_expires -ne $null) -and ($password_never_expires -ne $user_obj.PasswordNeverExpires)) { + Set-ADUser -Identity $username -PasswordNeverExpires $password_never_expires -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + If (($password_expired -ne $null) -and ($password_expired -ne $user_obj.PasswordExpired)) { + Set-ADUser -Identity $username -ChangePasswordAtLogon $password_expired -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + If (($user_cannot_change_password -ne $null) -and ($user_cannot_change_password -ne $user_obj.CannotChangePassword)) { + Set-ADUser -Identity $username -CannotChangePassword $user_cannot_change_password -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + + # Assign other account settings + If (($upn -ne $null) -and ($upn -ne $user_obj.UserPrincipalName)) { + Set-ADUser -Identity $username -UserPrincipalName $upn -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + If (($description -ne $null) -and ($description -ne $user_obj.Description)) { + Set-ADUser -Identity $username -description $description -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + If ($enabled -ne $user_obj.Enabled) { + Set-ADUser -Identity $username -Enabled $enabled -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + If ((-not $account_locked) -and ($user_obj.LockedOut -eq $true)) { + Unlock-ADAccount -Identity $username -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + + # Set user information + Foreach ($key in $user_info.Keys) { + If ($user_info[$key] -eq $null) { + continue + } + $value = $user_info[$key] + If ($value -ne $user_obj.$key) { + $expression = "Set-ADUser -Identity $username -$key '$value'" + If (-not $check_mode) { + Invoke-Expression $expression + } + $result.changed = $true + $user_obj = Get-ADUser -Identity $username -Properties * + } + } + + # Configure group assignment + If ($groups -ne $null) { + $group_list = $groups + + $groups = @() + Foreach ($group in $group_list) { + $groups += (Get-ADGroup -Identity $group).DistinguishedName + } + + $assigned_groups = @() + Foreach ($group in (Get-ADPrincipalGroupMembership -Identity $username)) { + $assigned_groups += $group.DistinguishedName + } + + switch ($groups_action) { + "add" { + Foreach ($group in $groups) { + If (-not ($assigned_groups -Contains $group)) { + Add-ADGroupMember -Identity $group -Members $username -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + } + } + "remove" { + Foreach ($group in $groups) { + If ($assigned_groups -Contains $group) { + Remove-ADGroupMember -Identity $group -Members $username -Confirm:$false -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + } + } + "replace" { + Foreach ($group in $assigned_groups) { + If (($group -ne $user_obj.PrimaryGroup) -and -not ($groups -Contains $group)) { + Remove-ADGroupMember -Identity $group -Members $username -Confirm:$false -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + } + Foreach ($group in $groups) { + If (-not ($assigned_groups -Contains $group)) { + Add-ADGroupMember -Identity $group -Members $username -WhatIf:$check_mode + $user_obj = Get-ADUser -Identity $username -Properties * + $result.changed = $true + } + } + } + } + } + + } + catch { + Fail-Json $result $_.Exception.Message + } +} ElseIf ($state -eq 'absent') { + # Ensure user does not exist + try { + If ($user_obj) { + Remove-ADUser $user_obj -Confirm:$false -WhatIf:$check_mode + $result.changed = $true + If ($check_mode) { + Exit-Json $result + } + $user_obj = $null + } + } + catch { + Fail-Json $result $_.Exception.Message + } +} + +try { + If ($user_obj) { + $user_obj = Get-ADUser -Identity $username -Properties * + $result.name = $user_obj.Name + $result.firstname = $user_obj.GivenName + $result.surname = $user_obj.Surname + $result.enabled = $user_obj.Enabled + $result.company = $user_obj.Company + $result.street = $user_obj.StreetAddress + $result.email = $user_obj.EmailAddress + $result.city = $user_obj.City + $result.state_province = $user_obj.State + $result.country = $user_obj.Country + $result.postal_code = $user_obj.PostalCode + $result.distinguished_name = $user_obj.DistinguishedName + $result.description = $user_obj.Description + $result.password_expired = $user_obj.PasswordExpired + $result.password_never_expires = $user_obj.PasswordNeverExpires + $result.user_cannot_change_password = $user_obj.CannotChangePassword + $result.account_locked = $user_obj.LockedOut + $result.sid = [string]$user_obj.SID + $result.upn = $user_obj.UserPrincipalName + $user_groups = @() + Foreach ($group in (Get-ADPrincipalGroupMembership $username)) { + $user_groups += $group.name + } + $result.groups = $user_groups + $result.msg = "User '$username' is present" + $result.state = "present" + } + Else { + $result.name = $username + $result.msg = "User '$username' is absent" + $result.state = "absent" + } +} +catch { + Fail-Json $result $_.Exception.Message +} + +Exit-Json $result \ No newline at end of file diff --git a/lib/ansible/modules/windows/win_domain_user.py b/lib/ansible/modules/windows/win_domain_user.py new file mode 100644 index 0000000000..ec469dd3a5 --- /dev/null +++ b/lib/ansible/modules/windows/win_domain_user.py @@ -0,0 +1,310 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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 . + +# 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_domain_user +version_added: '2.4' +short_description: Manages Windows Active Directory user accounts +description: + - Manages Windows Active Directory user accounts. +options: + name: + description: + - Name of the user to create, remove or modify. + required: true + state: + description: + - When C(present), creates or updates the user account. When C(absent), + removes the user account if it exists. When C(query), + retrieves the user account details without making any changes. + choices: + - present + - absent + - query + default: present + enabled: + description: + - C(yes) will enable the user account. C(no) will disable the account. + type: bool + default: 'yes' + account_locked: + description: + - C(no) will unlock the user account if locked. Note that there is not a + way to lock an account as an administrator. Accounts are locked due to + user actions; as an admin, you may only unlock a locked account. If you + wish to administratively disable an account, set 'enabled' to 'no'. + choices: [ 'no' ] + type: bool + description: + description: + - Description of the user + groups: + description: + - Adds or removes the user from this list of groups, + depending on the value of I(groups_action). To remove all but the + Principal Group, set C(groups=) and + I(groups_action=replace). Note that users cannot be removed from + their principal group (for example, "Domain Users"). + groups_action: + description: + - If C(replace), the user is added as a member of each group in + I(groups) and removed from any other groups. If C(add), the user is + added to each group in I(groups) where not already a member. If + C(remove), the user is removed from each group in I(groups). + choices: [ 'replace', 'add', 'remove' ] + default: replace + password: + description: + - Optionally set the user's password to this (plain text) value. In order + to enable an account - I(enabled) - a password must already be + configured on the account, or you must provide a password here. + update_password: + description: + - C(always) will update passwords if they differ. C(on_create) will + only set the password for newly created users. Note that C(always) will + always report an Ansible status of 'changed' because we cannot + determine whether the new password differs from the old password. + choices: [ 'always', 'on_create' ] + default: always + password_expired: + description: + - C(yes) will require the user to change their password at next login. + C(no) will clear the expired password flag. This is mutually exclusive + with I(password_never_expires). + type: bool + password_never_expires: + description: + - C(yes) will set the password to never expire. C(no) will allow the + password to expire. This is mutually exclusive with I(password_expired) + type: bool + user_cannot_change_password: + description: + - C(yes) will prevent the user from changing their password. C(no) will + allow the user to change their password. + type: bool + firstname: + description: + - Configures the user's first name (given name) + surname: + description: + - Configures the user's last name (surname) + company: + description: + - Configures the user's company name + upn: + description: + - Configures the User Principal Name (UPN) for the account. This is not + required, but is best practice to configure for modern versions of + Active Directory. The format is "@". + email: + description: + - Configures the user's email address. This is a record in AD and does + not do anything to configure any email servers or systems. + street: + description: + - Configures the user's street address + city: + description: + - Configures the user's city + state_province: + description: + - Configures the user's state or province + postal_code: + description: + - Configures the user's postal code / zip code + country: + description: + - Configures the user's country code. Note that this is a two-character + ISO 3166 code. + path: + description: + - Container or OU for the new user; if you do not specify this, the + user will be placed in the default container for users in the domain. + Setting the path is only available when a new user is created; + if you specify a path on an existing user, the user's path will not + be updated - you must delete (e.g., state=absent) the user and + then re-add the user with the appropriate path. +notes: + - Works with Windows 2012R2 and newer. + - If running on a server that is not a Domain Controller, credential + delegation through CredSSP or Kerberos with delegation must be used. + - Note that some individuals have confirmed successful operation on Windows + 2008R2 servers with AD and AD Web Services enabled, but this has not + received the same degree of testing as Windows 2012R2. +author: + - Nick Chandler (@nwchandler) +''' + +EXAMPLES = r''' +- name: Ensure user bob is present with address information + win_domain_user: + name: bob + firstname: Bob + surname: Smith + company: BobCo + password: B0bP4ssw0rd + state: present + groups: + - Domain Admins + street: 123 4th St. + city: Sometown + state_province: IN + postal_code: 12345 + country: US + +- name: Ensure user bob is present in OU ou=test,dc=domain,dc=local + win_domain_user: + name: bob + password: B0bP4ssw0rd + state: present + path: ou=test,dc=domain,dc=local + groups: + - Domain Admins + +- name: Ensure user bob is absent + win_domain_user: + name: bob + state: absent +''' + +RETURN = r''' +account_locked: + description: true if the account is locked + returned: always + type: boolean + sample: false +changed: + description: true if the account changed during execution + returned: always + type: boolean + sample: false +city: + description: The user city + returned: always + type: string + sample: Indianapolis +company: + description: The user company + returned: always + type: string + sample: RedHat +country: + description: The user country + returned: always + type: string + sample: US +description: + description: A description of the account + returned: always + type: string + sample: Server Administrator +distinguished_name: + description: DN of the user account + returned: always + type: string + sample: CN=nick,OU=test,DC=domain,DC=local +email: + description: The user email address + returned: always + type: string + sample: nick@domain.local +enabled: + description: true if the account is enabled and false if disabled + returned: always + type: string + sample: true +firstname: + description: The user first name + returned: always + type: string + sample: Nick +groups: + description: AD Groups to which the account belongs + returned: always + type: list + sample: [ "Domain Admins", "Domain Users" ] +msg: + description: Summary message of whether the user is present or absent + returned: always + type: string + sample: User nick is present +name: + description: The username on the account + returned: always + type: string + sample: nick +password_expired: + description: true if the account password has expired + returned: always + type: boolean + sample: false +password_updated: + description: true if the password changed during this execution + returned: always + type: boolean + sample: true +postal_code: + description: The user postal code + returned: always + type: string + sample: 46033 +sid: + description: The SID of the account + returned: always + type: string + sample: S-1-5-21-2752426336-228313920-2202711348-1175 +state: + description: The state of the user account + returned: always + type: string + sample: present +state_province: + description: The user state or province + returned: always + type: string + sample: IN +street: + description: The user street address + returned: always + type: string + sample: 123 4th St. +surname: + description: The user last name + returned: always + type: string + sample: Doe +upn: + description: The User Principal Name of the account + returned: always + type: string + sample: nick@domain.local +user_cannot_change_password: + description: true if the user is not allowed to change password + returned: always + type: string + sample: false +'''