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
+'''