mirror of
https://github.com/saltstack/salt.git
synced 2025-04-16 09:40:20 +00:00
fixes saltstack/salt#62856 add password/account locking/unlocking in user.present state on supported operating systems
This commit is contained in:
parent
8f27d32a1f
commit
4e1bbec71d
3 changed files with 176 additions and 0 deletions
1
changelog/62856.added
Normal file
1
changelog/62856.added
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add password/account locking/unlocking in user.present state on supported operating systems
|
|
@ -78,6 +78,7 @@ def _changes(
|
||||||
win_description=None,
|
win_description=None,
|
||||||
allow_uid_change=False,
|
allow_uid_change=False,
|
||||||
allow_gid_change=False,
|
allow_gid_change=False,
|
||||||
|
password_lock=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Return a dict of the changes required for a user if the user is present,
|
Return a dict of the changes required for a user if the user is present,
|
||||||
|
@ -158,6 +159,10 @@ def _changes(
|
||||||
change["warndays"] = warndays
|
change["warndays"] = warndays
|
||||||
if expire and lshad["expire"] != expire:
|
if expire and lshad["expire"] != expire:
|
||||||
change["expire"] = expire
|
change["expire"] = expire
|
||||||
|
if (password_lock and not lshad["passwd"].startswith("!")) or (
|
||||||
|
password_lock is False and lshad["passwd"].startswith("!")
|
||||||
|
):
|
||||||
|
change["password_lock"] = password_lock
|
||||||
elif "shadow.info" in __salt__ and salt.utils.platform.is_windows():
|
elif "shadow.info" in __salt__ and salt.utils.platform.is_windows():
|
||||||
if (
|
if (
|
||||||
expire
|
expire
|
||||||
|
@ -166,6 +171,8 @@ def _changes(
|
||||||
!= salt.utils.dateutils.strftime(expire)
|
!= salt.utils.dateutils.strftime(expire)
|
||||||
):
|
):
|
||||||
change["expire"] = expire
|
change["expire"] = expire
|
||||||
|
if password_lock is False and lusr["account_locked"]:
|
||||||
|
change["password_lock"] = password_lock
|
||||||
|
|
||||||
# GECOS fields
|
# GECOS fields
|
||||||
fullname = salt.utils.data.decode(fullname)
|
fullname = salt.utils.data.decode(fullname)
|
||||||
|
@ -266,6 +273,7 @@ def present(
|
||||||
nologinit=False,
|
nologinit=False,
|
||||||
allow_uid_change=False,
|
allow_uid_change=False,
|
||||||
allow_gid_change=False,
|
allow_gid_change=False,
|
||||||
|
password_lock=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Ensure that the named user is present with the specified properties
|
Ensure that the named user is present with the specified properties
|
||||||
|
@ -368,6 +376,14 @@ def present(
|
||||||
empty_password
|
empty_password
|
||||||
Set to True to enable password-less login for user, Default is ``False``.
|
Set to True to enable password-less login for user, Default is ``False``.
|
||||||
|
|
||||||
|
password_lock
|
||||||
|
Set to ``False`` to unlock a user's password (or Windows account). On
|
||||||
|
non-Windows systems ONLY, this parameter can be set to ``True`` to lock
|
||||||
|
a user's password. Default is ``None``, which does not take action on
|
||||||
|
the password (or Windows account).
|
||||||
|
|
||||||
|
.. versionadded:: 3006.0
|
||||||
|
|
||||||
shell
|
shell
|
||||||
The login shell, defaults to the system default shell
|
The login shell, defaults to the system default shell
|
||||||
|
|
||||||
|
@ -597,6 +613,7 @@ def present(
|
||||||
win_description,
|
win_description,
|
||||||
allow_uid_change,
|
allow_uid_change,
|
||||||
allow_gid_change,
|
allow_gid_change,
|
||||||
|
password_lock=password_lock,
|
||||||
)
|
)
|
||||||
except CommandExecutionError as exc:
|
except CommandExecutionError as exc:
|
||||||
ret["result"] = False
|
ret["result"] = False
|
||||||
|
@ -633,6 +650,17 @@ def present(
|
||||||
if changes.pop("empty_password", False) is True:
|
if changes.pop("empty_password", False) is True:
|
||||||
__salt__["shadow.del_password"](name)
|
__salt__["shadow.del_password"](name)
|
||||||
|
|
||||||
|
if "password_lock" in changes:
|
||||||
|
passlock = changes.pop("password_lock")
|
||||||
|
if not passlock and salt.utils.platform.is_windows():
|
||||||
|
__salt__["shadow.unlock_account"](name)
|
||||||
|
elif not passlock:
|
||||||
|
__salt__["shadow.unlock_password"](name)
|
||||||
|
elif passlock and not salt.utils.platform.is_windows():
|
||||||
|
__salt__["shadow.lock_password"](name)
|
||||||
|
else:
|
||||||
|
log.warning("Account locking is not available on Windows.")
|
||||||
|
|
||||||
if "date" in changes:
|
if "date" in changes:
|
||||||
del changes["date"]
|
del changes["date"]
|
||||||
__salt__["shadow.set_date"](name, date)
|
__salt__["shadow.set_date"](name, date)
|
||||||
|
@ -766,6 +794,7 @@ def present(
|
||||||
win_description,
|
win_description,
|
||||||
allow_uid_change=True,
|
allow_uid_change=True,
|
||||||
allow_gid_change=True,
|
allow_gid_change=True,
|
||||||
|
password_lock=password_lock,
|
||||||
)
|
)
|
||||||
# allow_uid_change and allow_gid_change passed as True to avoid race
|
# allow_uid_change and allow_gid_change passed as True to avoid race
|
||||||
# conditions where a uid/gid is modified outside of Salt. If an
|
# conditions where a uid/gid is modified outside of Salt. If an
|
||||||
|
|
|
@ -7,6 +7,7 @@ import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import salt.states.user as user
|
import salt.states.user as user
|
||||||
|
import salt.utils.platform
|
||||||
from tests.support.mock import MagicMock, Mock, patch
|
from tests.support.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -313,3 +314,148 @@ def test_gecos_field_changes_in_user_present():
|
||||||
):
|
):
|
||||||
res = user.present("Foo", homephone=44566, fullname="Bar Bar")
|
res = user.present("Foo", homephone=44566, fullname="Bar Bar")
|
||||||
assert res["changes"] == {"homephone": "44566", "fullname": "Bar Bar"}
|
assert res["changes"] == {"homephone": "44566", "fullname": "Bar Bar"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_present_password_lock_test_mode():
|
||||||
|
ret = {
|
||||||
|
"name": "salt",
|
||||||
|
"changes": {},
|
||||||
|
"result": True,
|
||||||
|
"comment": "User salt is present and up to date",
|
||||||
|
}
|
||||||
|
mock_info = MagicMock(
|
||||||
|
return_value={
|
||||||
|
"uid": 5000,
|
||||||
|
"gid": 5000,
|
||||||
|
"groups": [],
|
||||||
|
"home": "/home/salt",
|
||||||
|
"fullname": "Salty McSalterson",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
shadow_info = MagicMock(
|
||||||
|
side_effect=[
|
||||||
|
{"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": "!"},
|
||||||
|
{"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
shadow_hash = MagicMock(return_value="abcd")
|
||||||
|
|
||||||
|
with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict(
|
||||||
|
user.__salt__,
|
||||||
|
{
|
||||||
|
"shadow.default_hash": shadow_hash,
|
||||||
|
"shadow.info": shadow_info,
|
||||||
|
"user.info": mock_info,
|
||||||
|
"file.gid_to_group": MagicMock(return_value=5000),
|
||||||
|
},
|
||||||
|
), patch.dict(user.__opts__, {"test": True}):
|
||||||
|
assert user.present("salt", createhome=False, password_lock=True) == ret
|
||||||
|
ret.update(
|
||||||
|
{
|
||||||
|
"comment": "The following user attributes are set to be changed:\npassword_lock: True\n"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ret.update({"result": None})
|
||||||
|
assert user.present("salt", createhome=False, password_lock=True) == ret
|
||||||
|
|
||||||
|
|
||||||
|
def test_present_password_lock():
|
||||||
|
ret = {
|
||||||
|
"name": "salt",
|
||||||
|
"changes": {"passwd": "XXX-REDACTED-XXX"},
|
||||||
|
"result": True,
|
||||||
|
"comment": "Updated user salt",
|
||||||
|
}
|
||||||
|
mock_info = MagicMock(
|
||||||
|
return_value={
|
||||||
|
"uid": 5000,
|
||||||
|
"gid": 5000,
|
||||||
|
"groups": [],
|
||||||
|
"home": "/home/salt",
|
||||||
|
"fullname": "Salty McSalterson",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
shadow_info = MagicMock(
|
||||||
|
side_effect=[
|
||||||
|
{"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""},
|
||||||
|
{"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""},
|
||||||
|
{"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": "!"},
|
||||||
|
{"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": "!"},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
shadow_hash = MagicMock(return_value="abcd")
|
||||||
|
|
||||||
|
unlock_account = MagicMock()
|
||||||
|
unlock_password = MagicMock()
|
||||||
|
lock_password = MagicMock()
|
||||||
|
|
||||||
|
with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict(
|
||||||
|
user.__salt__,
|
||||||
|
{
|
||||||
|
"shadow.default_hash": shadow_hash,
|
||||||
|
"shadow.info": shadow_info,
|
||||||
|
"user.info": mock_info,
|
||||||
|
"file.gid_to_group": MagicMock(return_value=5000),
|
||||||
|
"shadow.unlock_account": unlock_account,
|
||||||
|
"shadow.unlock_password": unlock_password,
|
||||||
|
"shadow.lock_password": lock_password,
|
||||||
|
},
|
||||||
|
), patch.dict(user.__opts__, {"test": False}):
|
||||||
|
assert user.present("salt", createhome=False, password_lock=True) == ret
|
||||||
|
unlock_password.assert_not_called()
|
||||||
|
unlock_account.assert_not_called()
|
||||||
|
if salt.utils.platform.is_windows():
|
||||||
|
lock_password.assert_not_called()
|
||||||
|
else:
|
||||||
|
lock_password.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_present_password_unlock():
|
||||||
|
ret = {
|
||||||
|
"name": "salt",
|
||||||
|
"changes": {"passwd": "XXX-REDACTED-XXX"},
|
||||||
|
"result": True,
|
||||||
|
"comment": "Updated user salt",
|
||||||
|
}
|
||||||
|
mock_info = MagicMock(
|
||||||
|
return_value={
|
||||||
|
"uid": 5000,
|
||||||
|
"gid": 5000,
|
||||||
|
"groups": [],
|
||||||
|
"home": "/home/salt",
|
||||||
|
"fullname": "Salty McSalterson",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
shadow_info = MagicMock(
|
||||||
|
side_effect=[
|
||||||
|
{"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": "!"},
|
||||||
|
{"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": "!"},
|
||||||
|
{"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""},
|
||||||
|
{"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
shadow_hash = MagicMock(return_value="abcd")
|
||||||
|
|
||||||
|
unlock_account = MagicMock()
|
||||||
|
unlock_password = MagicMock()
|
||||||
|
lock_password = MagicMock()
|
||||||
|
with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict(
|
||||||
|
user.__salt__,
|
||||||
|
{
|
||||||
|
"shadow.default_hash": shadow_hash,
|
||||||
|
"shadow.info": shadow_info,
|
||||||
|
"user.info": mock_info,
|
||||||
|
"file.gid_to_group": MagicMock(return_value=5000),
|
||||||
|
"shadow.unlock_account": unlock_account,
|
||||||
|
"shadow.unlock_password": unlock_password,
|
||||||
|
"shadow.lock_password": lock_password,
|
||||||
|
},
|
||||||
|
), patch.dict(user.__opts__, {"test": False}):
|
||||||
|
assert user.present("salt", createhome=False, password_lock=False) == ret
|
||||||
|
lock_password.assert_not_called()
|
||||||
|
if salt.utils.platform.is_windows():
|
||||||
|
unlock_account.assert_called_once()
|
||||||
|
unlock_password.assert_not_called()
|
||||||
|
else:
|
||||||
|
unlock_password.assert_called_once()
|
||||||
|
unlock_account.assert_not_called()
|
||||||
|
|
Loading…
Add table
Reference in a new issue