Merge pull request #24984 from twangboy/win_user

Win user
This commit is contained in:
Thomas S Hatch 2015-06-30 14:54:50 -06:00
commit c940dbcb03
4 changed files with 410 additions and 101 deletions

View file

@ -2893,7 +2893,7 @@ def remove(path):
path = os.path.expanduser(path)
if not os.path.isabs(path):
raise SaltInvocationError('File path must be absolute.')
raise SaltInvocationError('File path must be absolute: {0}'.format(path))
try:
if os.path.isfile(path) or os.path.islink(path):

View file

@ -1,6 +1,16 @@
# -*- coding: utf-8 -*-
'''
Manage Windows users with the net user command
Module for managing Windows Users
:depends:
- os
- pywintypes
- win32api
- win32net
- win32netcon
- win32profile
- win32security
- win32ts
NOTE: This currently only works with local user accounts, not domain accounts
'''
@ -15,9 +25,14 @@ import logging
log = logging.getLogger(__name__)
try:
import os
import pywintypes
import win32api
import win32net
import win32netcon
import win32profile
import win32security
import win32ts
HAS_WIN32NET_MODS = True
except ImportError:
HAS_WIN32NET_MODS = False
@ -30,33 +45,54 @@ 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 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 name: str
User name
:param password: str
User's password in plain text.
:param fullname: str
The user's full name.
:param description: str
A brief description of the user account.
:param groups: list
A list of groups to add the user to.
:param home: str
The path to the user's home directory.
:param homedrive: str
The drive letter to assign to the home directory. Must be the Drive Letter
followed by a colon. ie: U:
:param profile: str
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 logonscript: str
Path to a login script to run when the user logs on.
:return: bool
True if successful. False is unsuccessful.
CLI Example:
@ -64,28 +100,148 @@ 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))
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)
if groups:
chgroups(name, groups)
ret = chgroups(name, groups)
return ret
def update(name,
password=None,
fullname=None,
description=None,
home=None,
homedrive=None,
logonscript=None,
profile=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 name: str
The user name to update.
:param password: str
New user password in plain text.
:param fullname: str
The user's full name.
:param description: str
A brief description of the user account.
:param home: str
The path to the user's home directory.
:param homedrive: str
The drive letter to assign to the home directory. Must be the Drive Letter
followed by a colon. ie: U:
:param logonscript: str
The path to the logon script.
:param profile: str
The path to the user's profile directory.
:return: bool
True if successful. False is unsuccessful.
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
# 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 name:
The name of the user to delete
:param 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 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.
CLI Example:
@ -93,8 +249,98 @@ 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:
# If the profile is not defined, get the profile from the registry
if user_info['profile'] == '':
profiles_dir = __salt__['reg.read_key'](hkey='HKEY_LOCAL_MACHINE',
path='SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList',
key='ProfilesDirectory')
profiles_dir = profiles_dir.replace('%SystemDrive%', os.environ['SystemDrive'])
user_info['profile'] = r'{0}\{1}'.format(profiles_dir, name)
# Make sure the profile exists before deleting it
# Otherwise this will throw an error
if os.path.exists(user_info['profile']):
sid = getUserSid(name)
try:
win32profile.DeleteProfile(sid)
except pywintypes.error as exc:
(number, context, message) = exc
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 getUserSid(username):
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):
@ -107,10 +353,7 @@ def setpassword(name, password):
salt '*' user.setpassword name password
'''
ret = __salt__['cmd.run_all'](
'net user {0} {1}'.format(name, password), output_loglevel='quiet'
)
return ret['retcode'] == 0
return update(name=name, password=password)
def addgroup(name, group):
@ -177,8 +420,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:
@ -203,22 +445,7 @@ def chprofile(name, profile):
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):
@ -231,22 +458,7 @@ def chfullname(name, fullname):
salt '*' user.chfullname user 'First Last'
'''
pre_info = info(name)
if not pre_info:
return False
if fullname == pre_info['fullname']:
return True
if __salt__['cmd.retcode']('net user {0} /fullname:"{1}"'.format(
name, fullname)) != 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=False):
@ -287,6 +499,25 @@ def info(name):
'''
Return user information
:param name: str
Username for which to display information
:returns: dict
A dictionary containing user information
- fullname
- username
- uid
- passwd (will always return None)
- comment (same as description, left here for backwards compatibility)
- description
- active
- logonscript
- profile
- home
- homedrive
- groups
- gid
CLI Example:
.. code-block:: bash
@ -312,12 +543,14 @@ 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']
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

View file

@ -72,11 +72,18 @@ def _changes(name,
maxdays=999999,
inactdays=0,
warndays=7,
expire=-1):
expire=-1,
win_homedrive=None,
win_profile=None,
win_logonscript=None,
win_description=None):
'''
Return a dict of the changes required for a user if the user is present,
otherwise return False.
Updated in 2015.8.0 to include support for windows homedrive, profile,
logonscript, and description fields.
Updated in 2014.7.0 to include support for shadow attributes, all
attributes supported as integers only.
'''
@ -117,7 +124,6 @@ def _changes(name,
newhome = home if home else lusr['home']
if newhome is not None and not os.path.isdir(newhome):
change['homeDoesNotExist'] = newhome
if shell:
if lusr['shell'] != shell:
change['shell'] = shell
@ -143,6 +149,19 @@ def _changes(name,
# GECOS fields
if fullname is not None and lusr['fullname'] != fullname:
change['fullname'] = fullname
if win_homedrive:
if lusr['homedrive'] != win_homedrive:
change['homedrive'] = win_homedrive
if win_profile:
if lusr['profile'] != win_profile:
change['profile'] = win_profile
if win_logonscript:
if lusr['logonscript'] != win_logonscript:
change['logonscript'] = win_logonscript
if win_description:
if lusr['description'] != win_description:
change['description'] = win_description
# MacOS doesn't have full GECOS support, so check for the "ch" functions
# and ignore these parameters if these functions do not exist.
if 'user.chroomnumber' in __salt__:
@ -189,7 +208,11 @@ def present(name,
maxdays=None,
inactdays=None,
warndays=None,
expire=None):
expire=None,
win_homedrive=None,
win_profile=None,
win_logonscript=None,
win_description=None):
'''
Ensure that the named user is present with the specified properties
@ -239,6 +262,7 @@ def present(name,
password
A password hash to set for the user. This field is only supported on
Linux, FreeBSD, NetBSD, OpenBSD, and Solaris.
For Windows this is the plain text password.
.. versionchanged:: 0.16.0
BSD support added.
@ -285,7 +309,6 @@ def present(name,
homephone
The user's home phone number (not supported in MacOS)
.. versionchanged:: 2014.7.0
Shadow attribute support added.
@ -313,8 +336,30 @@ def present(name,
expire
Date that account expires, represented in days since epoch (January 1,
1970).
'''
The below parameters apply to windows only:
win_homedrive (Windows Only)
The drive letter to use for the home directory. If not specified the
home directory will be a unc path. Otherwise the home directory will be
mapped to the specified drive. Must be a letter followed by a colon.
Because of the colon, the value must be surrounded by single quotes. ie:
- win_homedrive: 'U:
.. versionchanged:: 2015.8.0
win_profile (Windows Only)
The custom profile directory of the user. Uses default value of
underlying system if not set.
.. versionchanged:: 2015.8.0
win_logonscript (Windows Only)
The full path to the logon script to run when the user logs in.
.. versionchanged:: 2015.8.0
win_description (Windows Only)
A brief description of the purpose of the users account.
.. versionchanged:: 2015.8.0
'''
fullname = salt.utils.locales.sdecode(fullname)
roomnumber = salt.utils.locales.sdecode(roomnumber)
workphone = salt.utils.locales.sdecode(workphone)
@ -378,7 +423,11 @@ def present(name,
maxdays,
inactdays,
warndays,
expire)
expire,
win_homedrive,
win_profile,
win_logonscript,
win_description)
if changes:
if __opts__['test']:
@ -425,6 +474,18 @@ def present(name,
if key == 'expire':
__salt__['shadow.set_expire'](name, expire)
continue
if key == 'win_homedrive':
__salt__['user.update'](name=name, homedrive=val)
continue
if key == 'win_profile':
__salt__['user.update'](name=name, profile=val)
continue
if key == 'win_logonscript':
__salt__['user.update'](name=name, logonscript=val)
continue
if key == 'win_description':
__salt__['user.update'](name=name, description=val)
continue
if key == 'groups':
__salt__['user.ch{0}'.format(key)](
name, val, not remove_groups
@ -474,7 +535,11 @@ def present(name,
maxdays,
inactdays,
warndays,
expire)
expire,
win_homedrive,
win_profile,
win_logonscript,
win_description)
if changes:
ret['comment'] = 'These values could not be changed: {0}'.format(
@ -493,20 +558,36 @@ def present(name,
groups.extend(present_optgroups)
elif present_optgroups:
groups = present_optgroups[:]
if __salt__['user.add'](name,
uid=uid,
gid=gid,
groups=groups,
home=home,
shell=shell,
unique=unique,
system=system,
fullname=fullname,
roomnumber=roomnumber,
workphone=workphone,
homephone=homephone,
loginclass=loginclass,
createhome=createhome):
# Setup params specific to Linux and Windows to be passed to the
# add.user function
if not salt.utils.is_windows():
params = {'name': name,
'uid': uid,
'gid': gid,
'groups': groups,
'home': home,
'shell': shell,
'unique': unique,
'system': system,
'fullname': fullname,
'roomnumber': roomnumber,
'workphone': workphone,
'homephone': homephone,
'createhome': createhome,
'loginclass': loginclass}
else:
params = ({'name': name,
'password': password,
'fullname': fullname,
'description': win_description,
'groups': groups,
'home': home,
'homedrive': win_homedrive,
'profile': win_profile,
'logonscript': win_logonscript})
if __salt__['user.add'](**params):
ret['comment'] = 'New user {0} created'.format(name)
ret['changes'] = __salt__['user.info'](name)
if 'shadow.info' in __salt__ and not salt.utils.is_windows():
@ -575,11 +656,6 @@ def present(name,
ret['changes']['expire'] = expire
elif salt.utils.is_windows():
if password and not empty_password:
if not __salt__['user.setpassword'](name, password):
ret['comment'] = 'User {0} created but failed to set' \
' password to' \
' {1}'.format(name, password)
ret['result'] = False
ret['changes']['passwd'] = password
else:
ret['comment'] = 'Failed to create new user {0}'.format(name)

View file

@ -160,7 +160,7 @@ class FileModuleTest(integration.ModuleCase):
def test_cannot_remove(self):
ret = self.run_function('file.remove', arg=['tty'])
self.assertEqual(
'ERROR executing \'file.remove\': File path must be absolute.', ret
'ERROR executing \'file.remove\': File path must be absolute: tty', ret
)
def test_source_list_for_single_file_returns_unchanged(self):