Backport win_useradd

Backported win_useradd.py from 2015.8 to fix issues with unicode, etc.
This commit is contained in:
twangboy 2015-11-05 14:32:55 -07:00
parent 065f8c7fb3
commit 87282b6354

View File

@ -1,10 +1,22 @@
# -*- coding: utf-8 -*-
'''
Manage Windows users with the net user command
Module for managing Windows Users
NOTE: This currently only works with local user accounts, not domain accounts
:depends:
- pywintypes
- win32api
- win32net
- win32netcon
- win32profile
- win32security
- win32ts
.. note::
This currently only works with local user accounts, not domain accounts
'''
from __future__ import absolute_import
from datetime import datetime
import time
try:
from shlex import quote as _cmd_quote # pylint: disable=E0611
@ -20,14 +32,16 @@ import logging
log = logging.getLogger(__name__)
try:
import pywintypes
import wmi
import pythoncom
import pywintypes
import win32api
import win32con
import win32net
import win32netcon
import win32profile
import win32security
import win32ts
HAS_WIN32NET_MODS = True
except ImportError:
HAS_WIN32NET_MODS = False
@ -40,33 +54,108 @@ def __virtual__():
'''
Set the user module if the kernel is Windows
'''
if HAS_WIN32NET_MODS is True and salt.utils.is_windows():
if HAS_WIN32NET_MODS and salt.utils.is_windows():
return __virtualname__
return False
def _get_date_time_format(dt_string):
'''
Copied from win_system.py (_get_date_time_format)
Function that detects the date/time format for the string passed.
:param str dt_string:
A date/time string
:return: The format of the passed dt_string
:rtype: str
'''
valid_formats = [
'%Y-%m-%d %I:%M:%S %p',
'%m-%d-%y %I:%M:%S %p',
'%m-%d-%Y %I:%M:%S %p',
'%m/%d/%y %I:%M:%S %p',
'%m/%d/%Y %I:%M:%S %p',
'%Y/%m/%d %I:%M:%S %p',
'%Y-%m-%d %I:%M:%S',
'%m-%d-%y %I:%M:%S',
'%m-%d-%Y %I:%M:%S',
'%m/%d/%y %I:%M:%S',
'%m/%d/%Y %I:%M:%S',
'%Y/%m/%d %I:%M:%S',
'%Y-%m-%d %I:%M %p',
'%m-%d-%y %I:%M %p',
'%m-%d-%Y %I:%M %p',
'%m/%d/%y %I:%M %p',
'%m/%d/%Y %I:%M %p',
'%Y/%m/%d %I:%M %p',
'%Y-%m-%d %I:%M',
'%m-%d-%y %I:%M',
'%m-%d-%Y %I:%M',
'%m/%d/%y %I:%M',
'%m/%d/%Y %I:%M',
'%Y/%m/%d %I:%M',
'%Y-%m-%d',
'%m-%d-%y',
'%m-%d-%Y',
'%m/%d/%y',
'%m/%d/%Y',
'%Y/%m/%d',
]
for dt_format in valid_formats:
try:
datetime.strptime(dt_string, dt_format)
return dt_format
except ValueError:
continue
return False
def add(name,
password=None,
# Disable pylint checking on the next options. They exist to match the
# user modules of other distributions.
# pylint: disable=W0613
uid=None,
gid=None,
groups=None,
home=False,
shell=None,
unique=False,
system=False,
fullname=False,
roomnumber=False,
workphone=False,
homephone=False,
loginclass=False,
createhome=False
# pylint: enable=W0613
):
description=None,
groups=None,
home=None,
homedrive=None,
profile=None,
logonscript=None):
'''
Add a user to the minion
Add a user to the minion.
:param str name:
User name
:param str password:
User's password in plain text.
:param str fullname:
The user's full name.
:param str description:
A brief description of the user account.
:param list groups:
A list of groups to add the user to.
:param str home:
The path to the user's home directory.
:param str homedrive:
The drive letter to assign to the home directory. Must be the Drive Letter
followed by a colon. ie: U:
:param str profile:
An explicit path to a profile. Can be a UNC or a folder on the system. If
left blank, windows uses it's default profile directory.
:param str logonscript:
Path to a login script to run when the user logs on.
:return:
True if successful. False is unsuccessful.
:rtype: bool
CLI Example:
@ -74,28 +163,212 @@ def add(name,
salt '*' user.add name password
'''
if password:
ret = __salt__['cmd.run_all']('net user {0} {1} /add /y'.format(name, password))
user_info = {}
if name:
user_info['name'] = name
else:
ret = __salt__['cmd.run_all']('net user {0} /add'.format(name))
if groups:
chgroups(name, groups)
return False
user_info['password'] = password
user_info['priv'] = win32netcon.USER_PRIV_USER
user_info['home_dir'] = home
user_info['comment'] = description
user_info['flags'] = win32netcon.UF_SCRIPT
user_info['script_path'] = logonscript
try:
win32net.NetUserAdd(None, 1, user_info)
except win32net.error as exc:
(number, context, message) = exc
log.error('Failed to create user {0}'.format(name))
log.error('nbr: {0}'.format(number))
log.error('ctx: {0}'.format(context))
log.error('msg: {0}'.format(message))
return False
update(name=name,
homedrive=homedrive,
profile=profile,
fullname=fullname)
ret = chgroups(name, groups) if groups else True
return ret
def update(name,
password=None,
fullname=None,
description=None,
home=None,
homedrive=None,
logonscript=None,
profile=None,
expiration_date=None,
expired=None,
account_disabled=None,
unlock_account=None,
password_never_expires=None,
disallow_change_password=None):
r'''
Updates settings for the windows user. Name is the only required parameter.
Settings will only be changed if the parameter is passed a value.
.. versionadded:: 2015.8.0
:param str name:
The user name to update.
:param str password:
New user password in plain text.
:param str fullname:
The user's full name.
:param str description:
A brief description of the user account.
:param str home:
The path to the user's home directory.
:param str homedrive:
The drive letter to assign to the home directory. Must be the Drive Letter
followed by a colon. ie: U:
:param str logonscript:
The path to the logon script.
:param str profile:
The path to the user's profile directory.
:param date expiration_date: The date and time when the account expires. Can
be a valid date/time string. To set to never expire pass the string 'Never'.
:param bool expired: Pass `True` to expire the account. The user will be
prompted to change their password at the next logon. Pass `False` to mark
the account as 'not expired'. You can't use this to negate the expiration if
the expiration was caused by the account expiring. You'll have to change
the `expiration_date` as well.
:param bool account_disabled: True disables the account. False enables the
account.
:param bool unlock_account: True unlocks a locked user account. False is
ignored.
:param bool password_never_expires: True sets the password to never expire.
False allows the password to expire.
:param bool disallow_change_password: True blocks the user from changing
the password. False allows the user to change the password.
:return:
True if successful. False is unsuccessful.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' user.update bob password=secret profile=C:\Users\Bob
home=\\server\homeshare\bob homedrive=U:
'''
# Make sure the user exists
# Return an object containing current settings for the user
try:
user_info = win32net.NetUserGetInfo(None, name, 4)
except win32net.error as exc:
(number, context, message) = exc
log.error('Failed to update user {0}'.format(name))
log.error('nbr: {0}'.format(number))
log.error('ctx: {0}'.format(context))
log.error('msg: {0}'.format(message))
return False
# Check parameters to update
# Update the user object with new settings
if password:
user_info['password'] = password
if home:
user_info['home_dir'] = home
if homedrive:
user_info['home_dir_drive'] = homedrive
if description:
user_info['comment'] = description
if logonscript:
user_info['script_path'] = logonscript
if fullname:
chfullname(name, fullname)
return ret['retcode'] == 0
user_info['full_name'] = fullname
if profile:
user_info['profile'] = profile
if expiration_date:
if expiration_date == 'Never':
user_info['acct_expires'] = win32netcon.TIMEQ_FOREVER
else:
date_format = _get_date_time_format(expiration_date)
if date_format:
dt_obj = datetime.strptime(expiration_date, date_format)
else:
return 'Invalid start_date'
user_info['acct_expires'] = time.mktime(dt_obj.timetuple())
if expired is not None:
if expired:
user_info['password_expired'] = 1
else:
user_info['password_expired'] = 0
if account_disabled is not None:
if account_disabled:
user_info['flags'] |= win32netcon.UF_ACCOUNTDISABLE
else:
user_info['flags'] ^= win32netcon.UF_ACCOUNTDISABLE
if unlock_account is not None:
if unlock_account:
user_info['flags'] ^= win32netcon.UF_LOCKOUT
if password_never_expires is not None:
if password_never_expires:
user_info['flags'] |= win32netcon.UF_DONT_EXPIRE_PASSWD
else:
user_info['flags'] ^= win32netcon.UF_DONT_EXPIRE_PASSWD
if disallow_change_password is not None:
if disallow_change_password:
user_info['flags'] |= win32netcon.UF_PASSWD_CANT_CHANGE
else:
user_info['flags'] ^= win32netcon.UF_PASSWD_CANT_CHANGE
# Apply new settings
try:
win32net.NetUserSetInfo(None, name, 4, user_info)
except win32net.error as exc:
(number, context, message) = exc
log.error('Failed to update user {0}'.format(name))
log.error('nbr: {0}'.format(number))
log.error('ctx: {0}'.format(context))
log.error('msg: {0}'.format(message))
return False
return True
def delete(name,
# Disable pylint checking on the next options. They exist to match
# the user modules of other distributions.
# pylint: disable=W0613
purge=False,
force=False
# pylint: enable=W0613
):
force=False):
'''
Remove a user from the minion
NOTE: purge and force have not been implemented on Windows yet
:param str name:
The name of the user to delete
:param bool purge:
Boolean value indicating that the user profile should also be removed when
the user account is deleted. If set to True the profile will be removed.
:param bool force:
Boolean value indicating that the user account should be deleted even if the
user is logged in. True will log the user out and delete user.
:return:
True if successful
:rtype: bool
CLI Example:
@ -103,35 +376,150 @@ def delete(name,
salt '*' user.delete name
'''
ret = __salt__['cmd.run_all']('net user {0} /delete'.format(name))
return ret['retcode'] == 0
# Check if the user exists
try:
user_info = win32net.NetUserGetInfo(None, name, 4)
except win32net.error as exc:
(number, context, message) = exc
log.error('User not found: {0}'.format(name))
log.error('nbr: {0}'.format(number))
log.error('ctx: {0}'.format(context))
log.error('msg: {0}'.format(message))
return False
# Check if the user is logged in
# Return a list of logged in users
try:
sess_list = win32ts.WTSEnumerateSessions()
except win32ts.error as exc:
(number, context, message) = exc
log.error('No logged in users found')
log.error('nbr: {0}'.format(number))
log.error('ctx: {0}'.format(context))
log.error('msg: {0}'.format(message))
# Is the user one that is logged in
logged_in = False
session_id = None
for sess in sess_list:
if win32ts.WTSQuerySessionInformation(None, sess['SessionId'], win32ts.WTSUserName) == name:
session_id = sess['SessionId']
logged_in = True
# If logged in and set to force, log the user out and continue
# If logged in and not set to force, return false
if logged_in:
if force:
try:
win32ts.WTSLogoffSession(win32ts.WTS_CURRENT_SERVER_HANDLE, session_id, True)
except win32ts.error as exc:
(number, context, message) = exc
log.error('User not found: {0}'.format(name))
log.error('nbr: {0}'.format(number))
log.error('ctx: {0}'.format(context))
log.error('msg: {0}'.format(message))
return False
else:
log.error('User {0} is currently logged in.'.format(name))
return False
# Remove the User Profile directory
if purge:
try:
sid = getUserSid(name)
win32profile.DeleteProfile(sid)
except pywintypes.error as exc:
(number, context, message) = exc
if number == 2: # Profile Folder Not Found
pass
else:
log.error('Failed to remove profile for {0}'.format(name))
log.error('nbr: {0}'.format(number))
log.error('ctx: {0}'.format(context))
log.error('msg: {0}'.format(message))
return False
# And finally remove the user account
try:
win32net.NetUserDel(None, name)
except win32net.error as exc:
(number, context, message) = exc
log.error('Failed to delete user {0}'.format(name))
log.error('nbr: {0}'.format(number))
log.error('ctx: {0}'.format(context))
log.error('msg: {0}'.format(message))
return False
return True
def setpassword(name, password):
def getUserSid(username):
'''
Set a user's password
Get the Security ID for the user
:param str username:
user name for which to look up the SID
:return:
Returns the user SID
:rtype: str
CLI Example:
.. code-block:: bash
salt '*' user.setpassword name password
salt '*' user.getUserSid jsnuffy
'''
ret = __salt__['cmd.run_all'](
'net user {0} {1}'.format(name, password), output_loglevel='quiet'
)
return ret['retcode'] == 0
domain = win32api.GetComputerName()
if username.find(u'\\') != -1:
domain = username.split(u'\\')[0]
username = username.split(u'\\')[-1]
domain = domain.upper()
return win32security.ConvertSidToStringSid(win32security.LookupAccountName(None, domain + u'\\' + username)[0])
def setpassword(name, password):
'''
Set the user's password
:param str name:
user name for which to set the password
:param str password:
the new password
:return:
True if successful. False is unsuccessful.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' user.setpassword jsnuffy sup3rs3cr3t
'''
return update(name=name, password=password)
def addgroup(name, group):
'''
Add user to a group
:param str name:
user name to add to the group
:param str group:
name of the group to which to add the user
:return:
True if successful. False is unsuccessful.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' user.addgroup username groupname
salt '*' user.addgroup jsnuffy 'Power Users'
'''
name = _cmd_quote(name)
group = _cmd_quote(group).lstrip('\'').rstrip('\'')
@ -152,11 +540,21 @@ def removegroup(name, group):
'''
Remove user from a group
:param str name:
user name to remove from the group
:param str group:
name of the group from which to remove the user
:return:
True if successful. False is unsuccessful.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' user.removegroup username groupname
salt '*' user.removegroup jsnuffy 'Power Users'
'''
name = _cmd_quote(name)
group = _cmd_quote(group).lstrip('\'').rstrip('\'')
@ -180,6 +578,19 @@ def chhome(name, home, persist=False):
Change the home directory of the user, pass True for persist to move files
to the new home directory if the old home directory exist.
:param str name:
name of the user whose home directory you wish to change
:param str home:
new location of the home directory
:param bool persist:
True to move the contents of the existing home directory to the new location
:return:
True if successful. False is unsuccessful.
:rtype: bool
CLI Example:
.. code-block:: bash
@ -194,8 +605,7 @@ def chhome(name, home, persist=False):
if home == pre_info['home']:
return True
if __salt__['cmd.retcode']('net user {0} /homedir:{1}'.format(
name, home)) != 0:
if not update(name=name, home=home):
return False
if persist and home is not None and pre_info['home'] is not None:
@ -214,59 +624,46 @@ def chprofile(name, profile):
'''
Change the profile directory of the user
:param str name:
name of the user whose profile you wish to change
:param str profile:
new location of the profile
:return:
True if successful. False is unsuccessful.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' user.chprofile foo \\\\fileserver\\profiles\\foo
'''
pre_info = info(name)
if not pre_info:
return False
if profile == pre_info['profile']:
return True
if __salt__['cmd.retcode']('net user {0} /profilepath:{1}'.format(
name, profile)) != 0:
return False
post_info = info(name)
if post_info['profile'] != pre_info['profile']:
return post_info['profile'] == profile
return False
return update(name=name, profile=profile)
def chfullname(name, fullname):
'''
Change the full name of the user
:param str name:
user name for which to change the full name
:param str fullname:
the new value for the full name
:return:
True if successful. False is unsuccessful.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' user.chfullname user 'First Last'
'''
pre_info = info(name)
if not pre_info:
return False
name = _cmd_quote(name)
fullname_qts = _cmd_quote(fullname).replace("'", "\"")
if fullname == pre_info['fullname']:
return True
if __salt__['cmd.retcode']('net user {0} /fullname:{1}'.format(
name, fullname_qts), python_shell=True) != 0:
return False
post_info = info(name)
if post_info['fullname'] != pre_info['fullname']:
return post_info['fullname'] == fullname
return False
return update(name=name, fullname=fullname)
def chgroups(name, groups, append=True):
@ -274,11 +671,26 @@ def chgroups(name, groups, append=True):
Change the groups this user belongs to, add append=False to make the user a
member of only the specified groups
:param str name:
user name for which to change groups
:param groups:
a single group or a list of groups to assign to the user
:type groups: list, str
:param bool append:
True adds the passed groups to the user's current groups
False sets the user's groups to the passed groups only
:return:
True if successful. False is unsuccessful.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' user.chgroups foo wheel,root True
salt '*' user.chgroups jsnuffy Administrators,Users True
'''
if isinstance(groups, string_types):
groups = groups.split(',')
@ -312,11 +724,39 @@ def info(name):
'''
Return user information
:param str name:
Username for which to display information
:returns:
A dictionary containing user information
- fullname
- username
- SID
- passwd (will always return None)
- comment (same as description, left here for backwards compatibility)
- description
- active
- logonscript
- profile
- home
- homedrive
- groups
- password_changed
- successful_logon_attempts
- failed_logon_attempts
- last_logon
- account_disabled
- account_locked
- password_never_expires
- disallow_change_password
- gid
:rtype: dict
CLI Example:
.. code-block:: bash
salt '*' user.info root
salt '*' user.info jsnuffy
'''
ret = {}
items = {}
@ -337,19 +777,55 @@ def info(name):
ret['uid'] = win32security.ConvertSidToStringSid(items['user_sid'])
ret['passwd'] = items['password']
ret['comment'] = items['comment']
ret['description'] = items['comment']
ret['active'] = (not bool(items['flags'] & win32netcon.UF_ACCOUNTDISABLE))
ret['logonscript'] = items['script_path']
ret['profile'] = items['profile']
ret['failed_logon_attempts'] = items['bad_pw_count']
ret['successful_logon_attempts'] = items['num_logons']
secs = time.mktime(datetime.now().timetuple()) - items['password_age']
ret['password_changed'] = datetime.fromtimestamp(secs). \
strftime('%Y-%m-%d %H:%M:%S')
if items['last_logon'] == 0:
ret['last_logon'] = 'Never'
else:
ret['last_logon'] = datetime.fromtimestamp(items['last_logon']).\
strftime('%Y-%m-%d %H:%M:%S')
ret['expiration_date'] = datetime.fromtimestamp(items['acct_expires']).\
strftime('%Y-%m-%d %H:%M:%S')
ret['expired'] = items['password_expired'] == 1
if not ret['profile']:
ret['profile'] = _get_userprofile_from_registry(name, ret['uid'])
ret['home'] = items['home_dir']
ret['homedrive'] = items['home_dir_drive']
if not ret['home']:
ret['home'] = ret['profile']
ret['groups'] = groups
if items['flags'] & win32netcon.UF_DONT_EXPIRE_PASSWD == 0:
ret['password_never_expires'] = False
else:
ret['password_never_expires'] = True
if items['flags'] & win32netcon.UF_ACCOUNTDISABLE == 0:
ret['account_disabled'] = False
else:
ret['account_disabled'] = True
if items['flags'] & win32netcon.UF_LOCKOUT == 0:
ret['account_locked'] = False
else:
ret['account_locked'] = True
if items['flags'] & win32netcon.UF_PASSWD_CANT_CHANGE == 0:
ret['disallow_change_password'] = False
else:
ret['disallow_change_password'] = True
ret['gid'] = ''
return ret
else:
return False
def _get_userprofile_from_registry(user, sid):
'''
@ -369,6 +845,13 @@ def list_groups(name):
'''
Return a list of groups the named user belongs to
:param str name:
user name for which to list groups
:return:
list of groups to which the user belongs
:rtype: list
CLI Example:
.. code-block:: bash
@ -390,6 +873,14 @@ def getent(refresh=False):
'''
Return the list of all info for all users
:param bool refresh:
Refresh the cached user information. Default is False. Useful when used from
within a state function.
:return:
A dictionary containing information about all users on the system
:rtype: dict
CLI Example:
.. code-block:: bash
@ -421,6 +912,16 @@ def getent(refresh=False):
def list_users():
'''
Return a list of users on Windows
:return:
list of users on the system
:rtype: list
CLI Example:
.. code-block:: bash
salt '*' user.list_users
'''
res = 0
users = []
@ -447,11 +948,21 @@ def rename(name, new_name):
'''
Change the username for a named user
:param str name:
user name to change
:param str new_name:
the new name for the current user
:return:
True if successful. False is unsuccessful.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' user.rename name new_name
salt '*' user.rename jsnuffy jshmoe
'''
# Load information for the current name
current_info = info(name)