mathiasertl / django-hashers-passlib

Provide Django hashers for passlib hashes
GNU General Public License v3.0
18 stars 12 forks source link

Share a patch for Django 4.2 #16

Open yarin-zhang opened 5 months ago

yarin-zhang commented 5 months ago

I've been using this module to add pbkdf2_sha256 support for my Django project.

However, when I upgraded Django from 3.2 to 4.2, I encountered a problem due to the deprecation of ugettext_noop in Django 4.2, which caused errors in the project. Aside from this, Passlib worked well without any other issues.

Since both this project and its dependency, passlib, invoked ugettext_noop, it seemed that simply updating the reference to ugettext_noop (actually just replacing ugettext_noop with gettext_noop) might solve the problem.

But I found that both libraries haven't been updated for a while, and just submitting a PR here wouldn't fix the issues in both libraries. So, I tried using a patching approach to address this issue and am sharing it for others who might encounter the same problem.


My Approach

You'll need to create a new app, for example, passlib_patch, and then add a patches.py file.

from django.utils.translation import gettext_noop as _

# This file is a patch for django-hashers-passlib==0.4 and passlib==1.7.4 with Django 4.2. 
# These packages have not been updated for a long time and do not support Django 4.2.

def apply_patches():
    # Patch for passlib.ext.django.utils
    try:
        from passlib.ext.django.utils import _PasslibHasherWrapper

        # Patched safe_summary method to use the patched _ function instead of the original ugettext_noop
        def patched_safe_summary(self, encoded):
            from collections import OrderedDict
            from django.contrib.auth.hashers import mask_hash
            handler = self.passlib_handler
            items = [
                (_('algorithm'), handler.name),
            ]
            if hasattr(handler, "parsehash"):
                kwds = handler.parsehash(encoded, sanitize=mask_hash)
                for key, value in kwds.items():
                    key = self._translate_kwds.get(key, key)
                    items.append((_(key), value))
            return OrderedDict(items)

        _PasslibHasherWrapper.safe_summary = patched_safe_summary
    except ImportError as e:
        # Handle the case where passlib is not installed
        print(f"Failed to apply patch for passlib: {e}")

    # Patch for hashers_passlib
    try:
        # Check if ugettext_noop already exists, if not, add it
        from django.utils.translation import ugettext_noop
    except ImportError:
        # Dynamically add ugettext_noop pointing to gettext_noop in the django.utils.translation module
        import django.utils.translation
        django.utils.translation.ugettext_noop = _

        # Now safely import hashers_passlib since ugettext_noop has been added
        import hashers_passlib

        # Since we've successfully imported hashers_passlib, this indicates the patch has taken effect,
        # If necessary, original state can be restored here
        # However, typically, if ugettext_noop is not directly used elsewhere in the Django project, restoration may not be needed

    except Exception as e:
        print(f"Unexpected error while applying patch for hashers_passlib: {e}")

Then add the following code in apps.py:

from django.apps import AppConfig

class PasslibPatchConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'passlib_patch'

    def ready(self):
        from .patches import apply_patches
        apply_patches()

And add in __init__.py:

from . import patches

default_app_config = 'passlib_patch.apps.PasslibPatchConfig'

I wrote a tests.py file and passed the tests.

from django.test import TestCase
from django.contrib.auth import get_user_model
from passlib.hash import django_pbkdf2_sha256 as handler
from wp_user.patches import apply_patches

apply_patches()

class PasslibHashersTestCase(TestCase):
    def test_passlib_functionality(self):
        hashed = handler.hash("s3cr3t")
        self.assertTrue(handler.verify("s3cr3t", hashed), "Password verification failed!")

    def test_hashers_passlib_integration(self):
        User = get_user_model()
        user = User(username='testuser')
        user.set_password('my_password')
        user.save()

        self.assertTrue(user.check_password('my_password'), "User password verification failed!")

I can't guarantee that this code will work for everyone, but this patch did fix the error for me.

If there are any questions, feel free to discuss.

n2o commented 4 months ago

Same problem here with an old project...

We are currently migrating to Poetry, which supports git dependencies. I'll go with this solution for now:

[tool.poetry.dependencies]
# ...
django-hashers-passlib = { git = "https://github.com/mathiasertl/django-hashers-passlib.git" }
bartTC commented 2 months ago

I forked the project and named it django-hashers-passlib-revived. It has support for up to Python 3.12 and Django 5.0.