diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 24940c692c..bdfc5f8169 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -379,6 +379,8 @@ files: $module_utils/jenkins.py: labels: jenkins maintainers: russoz + $module_utils/_crypt.py: + maintainers: russoz $module_utils/_lxc.py: maintainers: russoz $module_utils/_lvm.py: diff --git a/changelogs/fragments/11860-udm_user-replace-crypt.yml b/changelogs/fragments/11860-udm_user-replace-crypt.yml new file mode 100644 index 0000000000..c7e9aa2958 --- /dev/null +++ b/changelogs/fragments/11860-udm_user-replace-crypt.yml @@ -0,0 +1,6 @@ +bugfixes: + - homectl - allow to use passlib instead of legacycrypt for Python 3.13+ + (https://github.com/ansible-collections/community.general/pull/11860). + - udm_user - allow to use passlib instead of legacycrypt for Python 3.13+ + (https://github.com/ansible-collections/community.general/issues/4690, + https://github.com/ansible-collections/community.general/pull/11860). diff --git a/plugins/module_utils/_crypt.py b/plugins/module_utils/_crypt.py new file mode 100644 index 0000000000..f362388e65 --- /dev/null +++ b/plugins/module_utils/_crypt.py @@ -0,0 +1,45 @@ +# Copyright (c) 2026, Alexei Znamensky +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +__all__ = ["CryptContext", "has_crypt_context"] + +has_crypt_context = True +try: + from passlib.context import CryptContext + +except ImportError: + try: + try: + import crypt as _crypt_mod + except ImportError: + import legacycrypt as _crypt_mod + + _SCHEME_TO_METHOD = { + "sha512_crypt": _crypt_mod.METHOD_SHA512, + "sha256_crypt": _crypt_mod.METHOD_SHA256, + "md5_crypt": _crypt_mod.METHOD_MD5, + "des_crypt": _crypt_mod.METHOD_CRYPT, + } + + class CryptContext: # type: ignore[no-redef] + @staticmethod + def verify(password, password_hash): + return _crypt_mod.crypt(password, password_hash) == password_hash + + @staticmethod + def hash(password, scheme="sha512_crypt", rounds=10000): + method = _SCHEME_TO_METHOD.get(scheme) + if method is None: + raise ValueError(f"Unsupported scheme: {scheme}") + salt = _crypt_mod.mksalt(method, rounds=rounds) + return _crypt_mod.crypt(password, salt) + + except ImportError: + + class CryptContext: # type: ignore[no-redef] + pass + + has_crypt_context = False diff --git a/plugins/modules/homectl.py b/plugins/modules/homectl.py index 0952fe09ac..470215dad4 100644 --- a/plugins/modules/homectl.py +++ b/plugins/modules/homectl.py @@ -15,10 +15,11 @@ version_added: 4.4.0 description: - Manages a user's home directory managed by systemd-homed. notes: - - This module requires the deprecated L(crypt Python module, https://docs.python.org/3.12/library/crypt.html) library which - was removed from Python 3.13. For Python 3.13 or newer, you need to install L(legacycrypt, https://pypi.org/project/legacycrypt/). + - This module uses L(passlib, https://pypi.org/project/passlib/) for password hashing when available, + falling back to the Python C(crypt) module or L(legacycrypt, https://pypi.org/project/legacycrypt/). requirements: - - legacycrypt (on Python 3.13 or newer) + - passlib (Python library, recommended), or legacycrypt on Python 3.13 or newer + - It requires no dependency on Python 3.12 and earlier, but then it relies on the deprecated standard library C(crypt). extends_documentation_fragment: - community.general.attributes attributes: @@ -268,38 +269,23 @@ data: """ import json -import traceback -from ansible.module_utils.basic import AnsibleModule, jsonify, missing_required_lib +from ansible.module_utils.basic import AnsibleModule, jsonify from ansible.module_utils.common.text.formatters import human_to_bytes -CRYPT_IMPORT_ERROR: str | None -try: - import crypt -except ImportError: - HAS_CRYPT = False - CRYPT_IMPORT_ERROR = traceback.format_exc() -else: - HAS_CRYPT = True - CRYPT_IMPORT_ERROR = None +from ansible_collections.community.general.plugins.module_utils import deps -LEGACYCRYPT_IMPORT_ERROR: str | None -try: - import legacycrypt +with deps.declare("crypt_context"): + from ansible_collections.community.general.plugins.module_utils._crypt import CryptContext, has_crypt_context - if not HAS_CRYPT: - crypt = legacycrypt -except ImportError: - HAS_LEGACYCRYPT = False - LEGACYCRYPT_IMPORT_ERROR = traceback.format_exc() -else: - HAS_LEGACYCRYPT = True - LEGACYCRYPT_IMPORT_ERROR = None + if not has_crypt_context: + raise ImportError("Failed to import any of: passlib, crypt, legacycrypt") class Homectl: - def __init__(self, module): + def __init__(self, module, crypt_context): self.module = module + self.crypt_context = crypt_context self.state = module.params["state"] self.name = module.params["name"] self.password = module.params["password"] @@ -364,14 +350,10 @@ class Homectl: return self.module.run_command(cmd, data=record) def _hash_password(self, password): - method = crypt.METHOD_SHA512 - salt = crypt.mksalt(method, rounds=10000) - pw_hash = crypt.crypt(password, salt) - return pw_hash + return self.crypt_context.hash(password, scheme="sha512_crypt", rounds=10000) def _check_password(self, pwhash): - hash = crypt.crypt(self.password, pwhash) - return pwhash == hash + return self.crypt_context.verify(self.password, pwhash) def remove_user(self): cmd = [self.module.get_bin_path("homectl", True)] @@ -616,13 +598,10 @@ def main(): ) module.run_command_environ_update = {"LANGUAGE": "C", "LC_ALL": "C"} - if not HAS_CRYPT and not HAS_LEGACYCRYPT: - module.fail_json( - msg=missing_required_lib("crypt (part of standard library up to Python 3.12) or legacycrypt (PyPI)"), - exception=CRYPT_IMPORT_ERROR, - ) + deps.validate(module) - homectl = Homectl(module) + crypt_context = CryptContext(schemes=["sha512_crypt", "sha256_crypt", "md5_crypt", "des_crypt"]) + homectl = Homectl(module, crypt_context) homectl.result["state"] = homectl.state # First we need to make sure homed service is active diff --git a/plugins/modules/udm_user.py b/plugins/modules/udm_user.py index e915637b57..d681c75cfe 100644 --- a/plugins/modules/udm_user.py +++ b/plugins/modules/udm_user.py @@ -16,10 +16,11 @@ description: - This module allows to manage posix users on a univention corporate server (UCS). It uses the Python API of the UCS to create a new object or edit it. notes: - - This module requires the deprecated L(crypt Python module, https://docs.python.org/3.12/library/crypt.html) library which - was removed from Python 3.13. For Python 3.13 or newer, you need to install L(legacycrypt, https://pypi.org/project/legacycrypt/). + - This module uses L(passlib, https://pypi.org/project/passlib/) for password hashing when available, + falling back to the Python C(crypt) module or L(legacycrypt, https://pypi.org/project/legacycrypt/). requirements: - - legacycrypt (on Python 3.13 or newer) + - passlib (Python library, recommended), or legacycrypt on Python 3.13 or newer + - It requires no dependency on Python 3.12 and earlier, but then it relies on the deprecated standard library C(crypt). extends_documentation_fragment: - community.general.attributes attributes: @@ -316,10 +317,18 @@ EXAMPLES = r""" RETURN = """#""" -import traceback + from datetime import date, timedelta -from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.general.plugins.module_utils import deps + +with deps.declare("crypt_context"): + from ansible_collections.community.general.plugins.module_utils._crypt import CryptContext, has_crypt_context + + if not has_crypt_context: + raise ImportError("Failed to import any of: passlib, crypt, legacycrypt") from ansible_collections.community.general.plugins.module_utils.univention_umc import ( base_dn, @@ -328,29 +337,6 @@ from ansible_collections.community.general.plugins.module_utils.univention_umc i umc_module_for_edit, ) -CRYPT_IMPORT_ERROR: str | None -try: - import crypt -except ImportError: - HAS_CRYPT = False - CRYPT_IMPORT_ERROR = traceback.format_exc() -else: - HAS_CRYPT = True - CRYPT_IMPORT_ERROR = None - -LEGACYCRYPT_IMPORT_ERROR: str | None -try: - import legacycrypt - - if not HAS_CRYPT: - crypt = legacycrypt -except ImportError: - HAS_LEGACYCRYPT = False - LEGACYCRYPT_IMPORT_ERROR = traceback.format_exc() -else: - HAS_LEGACYCRYPT = True - LEGACYCRYPT_IMPORT_ERROR = None - def main(): expiry = date.strftime(date.today() + timedelta(days=365), "%Y-%m-%d") @@ -410,11 +396,9 @@ def main(): required_if=([("state", "present", ["firstname", "lastname", "password"])]), ) - if not HAS_CRYPT and not HAS_LEGACYCRYPT: - module.fail_json( - msg=missing_required_lib("crypt (part of standard library up to Python 3.12) or legacycrypt (PyPI)"), - exception=LEGACYCRYPT_IMPORT_ERROR, - ) + deps.validate(module) + + crypt_context = CryptContext(schemes=["sha512_crypt", "sha256_crypt", "md5_crypt", "des_crypt"]) username = module.params["username"] position = module.params["position"] @@ -474,7 +458,7 @@ def main(): obj["password"] = password if module.params["update_password"] == "always": old_password = obj["password"].split("}", 2)[1] - if crypt.crypt(password, old_password) != old_password: + if not crypt_context.verify(password, old_password): obj["overridePWHistory"] = module.params["overridePWHistory"] obj["overridePWLength"] = module.params["overridePWLength"] obj["password"] = password diff --git a/tests/sanity/ignore-2.17.txt b/tests/sanity/ignore-2.17.txt index b96f5d20fc..0aaa630e66 100644 --- a/tests/sanity/ignore-2.17.txt +++ b/tests/sanity/ignore-2.17.txt @@ -1,12 +1,10 @@ +plugins/module_utils/_crypt.py import-3.11 # Uses deprecated stdlib library 'crypt' +plugins/module_utils/_crypt.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice -plugins/modules/homectl.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/homectl.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/iptables_state.py validate-modules:undocumented-parameter # params _back and _timeout used by action plugin plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/rhevm.py validate-modules:parameter-state-invalid-choice -plugins/modules/udm_user.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/udm_user.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/xfconf.py validate-modules:return-syntax-error plugins/plugin_utils/unsafe.py pep8:E704 tests/unit/plugins/modules/test_gio_mime.yaml no-smart-quotes diff --git a/tests/sanity/ignore-2.18.txt b/tests/sanity/ignore-2.18.txt index b96f5d20fc..0aaa630e66 100644 --- a/tests/sanity/ignore-2.18.txt +++ b/tests/sanity/ignore-2.18.txt @@ -1,12 +1,10 @@ +plugins/module_utils/_crypt.py import-3.11 # Uses deprecated stdlib library 'crypt' +plugins/module_utils/_crypt.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice -plugins/modules/homectl.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/homectl.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/iptables_state.py validate-modules:undocumented-parameter # params _back and _timeout used by action plugin plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/rhevm.py validate-modules:parameter-state-invalid-choice -plugins/modules/udm_user.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/udm_user.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/xfconf.py validate-modules:return-syntax-error plugins/plugin_utils/unsafe.py pep8:E704 tests/unit/plugins/modules/test_gio_mime.yaml no-smart-quotes diff --git a/tests/sanity/ignore-2.19.txt b/tests/sanity/ignore-2.19.txt index 561494d681..b4e28475a1 100644 --- a/tests/sanity/ignore-2.19.txt +++ b/tests/sanity/ignore-2.19.txt @@ -1,11 +1,9 @@ +plugins/module_utils/_crypt.py import-3.11 # Uses deprecated stdlib library 'crypt' +plugins/module_utils/_crypt.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice -plugins/modules/homectl.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/homectl.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/iptables_state.py validate-modules:undocumented-parameter # params _back and _timeout used by action plugin plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/rhevm.py validate-modules:parameter-state-invalid-choice -plugins/modules/udm_user.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/udm_user.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/xfconf.py validate-modules:return-syntax-error tests/unit/plugins/modules/test_gio_mime.yaml no-smart-quotes diff --git a/tests/sanity/ignore-2.20.txt b/tests/sanity/ignore-2.20.txt index 561494d681..b4e28475a1 100644 --- a/tests/sanity/ignore-2.20.txt +++ b/tests/sanity/ignore-2.20.txt @@ -1,11 +1,9 @@ +plugins/module_utils/_crypt.py import-3.11 # Uses deprecated stdlib library 'crypt' +plugins/module_utils/_crypt.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice -plugins/modules/homectl.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/homectl.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/iptables_state.py validate-modules:undocumented-parameter # params _back and _timeout used by action plugin plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/rhevm.py validate-modules:parameter-state-invalid-choice -plugins/modules/udm_user.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/udm_user.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/xfconf.py validate-modules:return-syntax-error tests/unit/plugins/modules/test_gio_mime.yaml no-smart-quotes diff --git a/tests/sanity/ignore-2.21.txt b/tests/sanity/ignore-2.21.txt index c64e926bca..f8c890e182 100644 --- a/tests/sanity/ignore-2.21.txt +++ b/tests/sanity/ignore-2.21.txt @@ -1,9 +1,9 @@ +plugins/module_utils/_crypt.py import-3.11 # Uses deprecated stdlib library 'crypt' +plugins/module_utils/_crypt.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/module_utils/_lxc.py pylint:ansible-bad-function # needs to use Popen() to stream logs plugins/modules/ansible_galaxy_install.py validate-modules:bad-return-value-key # TODO: rename offending return values if possible, or adjust this comment in case the name is OK plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice plugins/modules/gandi_livedns.py validate-modules:bad-return-value-key # TODO: rename offending return values if possible, or adjust this comment in case the name is OK -plugins/modules/homectl.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/homectl.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/interfaces_file.py validate-modules:bad-return-value-key # TODO: rename offending return values if possible, or adjust this comment in case the name is OK plugins/modules/iptables_state.py validate-modules:undocumented-parameter # params _back and _timeout used by action plugin plugins/modules/keycloak_realm_info.py validate-modules:bad-return-value-key # TODO: rename offending return values if possible, or adjust this comment in case the name is OK @@ -13,7 +13,5 @@ plugins/modules/omapi_host.py validate-modules:bad-return-value-key # TODO: ren plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/rhevm.py validate-modules:parameter-state-invalid-choice -plugins/modules/udm_user.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/udm_user.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/xfconf.py validate-modules:return-syntax-error tests/unit/plugins/modules/test_gio_mime.yaml no-smart-quotes diff --git a/tests/sanity/ignore-2.22.txt b/tests/sanity/ignore-2.22.txt index c64e926bca..f8c890e182 100644 --- a/tests/sanity/ignore-2.22.txt +++ b/tests/sanity/ignore-2.22.txt @@ -1,9 +1,9 @@ +plugins/module_utils/_crypt.py import-3.11 # Uses deprecated stdlib library 'crypt' +plugins/module_utils/_crypt.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/module_utils/_lxc.py pylint:ansible-bad-function # needs to use Popen() to stream logs plugins/modules/ansible_galaxy_install.py validate-modules:bad-return-value-key # TODO: rename offending return values if possible, or adjust this comment in case the name is OK plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice plugins/modules/gandi_livedns.py validate-modules:bad-return-value-key # TODO: rename offending return values if possible, or adjust this comment in case the name is OK -plugins/modules/homectl.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/homectl.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/interfaces_file.py validate-modules:bad-return-value-key # TODO: rename offending return values if possible, or adjust this comment in case the name is OK plugins/modules/iptables_state.py validate-modules:undocumented-parameter # params _back and _timeout used by action plugin plugins/modules/keycloak_realm_info.py validate-modules:bad-return-value-key # TODO: rename offending return values if possible, or adjust this comment in case the name is OK @@ -13,7 +13,5 @@ plugins/modules/omapi_host.py validate-modules:bad-return-value-key # TODO: ren plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/rhevm.py validate-modules:parameter-state-invalid-choice -plugins/modules/udm_user.py import-3.11 # Uses deprecated stdlib library 'crypt' -plugins/modules/udm_user.py import-3.12 # Uses deprecated stdlib library 'crypt' plugins/modules/xfconf.py validate-modules:return-syntax-error tests/unit/plugins/modules/test_gio_mime.yaml no-smart-quotes