Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: emit warning / exception from 'crypt_check' #47

Merged
merged 1 commit into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ nominated to act as authenticator plugins. ::
use = repoze.who.plugins.htpasswd:make_plugin
filename = %(here)s/passwd
check_fn = repoze.who.plugins.htpasswd:crypt_check
# The stdlib 'crypt' module is not available for Python > 3.13. Instead,
# use: 'bcrypt.checkpw' function (see https://pypi.org/projects/bcrypt/).
# check_fn = bcrypt:checkpw

[plugin:sqlusers]
# authentication
Expand Down
6 changes: 6 additions & 0 deletions docs/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ authentication, identification, challenge and metadata provision.
``repoze.who.plugins.htpasswd:crypt_check``; it assumes the values
in the htpasswd file are encrypted with the UNIX ``crypt`` function.

.. note::
The ``crypt`` module is not available in the standard library for
Python >= 3.13. Recommended replacement for the checker function
we provide here (``repoze.who.plugins.htpasswd:crypt_check``) is the
``bcrypt.checkpw`` function (see https://pypi.org/projects/bcrypt/).

.. module:: repoze.who.plugins.redirector

.. class:: RedirectorPlugin(login_url, came_from_param, reason_param, reason_header)
Expand Down
34 changes: 29 additions & 5 deletions repoze/who/plugins/htpasswd.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
try:
import crypt
except ImportError:
# Note: the crypt module is deprecated since Python 3.11
# and will be removed in Python 3.13.
# win32 does not have a crypt library at all.
HAS_CRYPT = False
else:
HAS_CRYPT = True
import itertools
import warnings

from zope.interface import implementer

Expand Down Expand Up @@ -90,13 +100,27 @@ def _same_string(x, y):
mismatches = list(mismatches)
return len(mismatches) == 0


if not HAS_CRYPT:

class CryptModuleNotImportable(RuntimeError):
def __init__(self):
super().__init__(
"'crypt' module is not importable. "
"Try 'bcrypt.checkpw' instead?"
)

def crypt_check(password, hashed):
# Note: the crypt module is deprecated since Python 3.11
# and will be removed in Python 3.13.
# win32 does not have a crypt library at all.
from crypt import crypt

if not HAS_CRYPT:
raise CryptModuleNotImportable()

warnings.warn(
"'crypt' module is deprecated -- try 'bcrypt.checkpw' instead?"
)
salt = hashed[:2]
return _same_string(hashed, crypt(password, salt))
return _same_string(hashed, crypt.crypt(password, salt))


def sha1_check(password, hashed):
from hashlib import sha1
Expand Down
41 changes: 35 additions & 6 deletions repoze/who/plugins/tests/test_htpasswd.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import unittest


try:
from crypt import crypt
except ImportError:
# The crypt module is deprecated since Python 3.11
# and will be removed in Python 3.13.
# win32 does not have a crypt library at all.
crypt = None
import unittest
import warnings

import pytest


class TestHTPasswdPlugin(unittest.TestCase):
Expand Down Expand Up @@ -116,12 +117,40 @@ def warn(self, msg):
self.assertTrue('could not open htpasswd' in logger.warnings[0])

@unittest.skipIf(crypt is None, "crypt module not available")
def test_crypt_check(self):
def test_crypt_check_hit(self):
from repoze.who.plugins.htpasswd import crypt_check
salt = '123'
hashed = crypt('password', salt)

with warnings.catch_warnings(record=True) as logged:
assert crypt_check('password', hashed)

assert len(logged) == 1
record = logged[0]
assert record.category is UserWarning
assert "'crypt' module is deprecated" in str(record.message)

@unittest.skipIf(crypt is None, "crypt module not available")
def test_crypt_check_miss(self):
from repoze.who.plugins.htpasswd import crypt_check
salt = '123'
hashed = crypt('password', salt)

with warnings.catch_warnings(record=True) as logged:
assert not crypt_check('notpassword', hashed)

assert len(logged) == 1
record = logged[0]
assert record.category is UserWarning
assert "'crypt' module is deprecated" in str(record.message)

@unittest.skipIf(crypt is not None, "crypt module available")
def test_crypt_check_gone(self):
from repoze.who.plugins.htpasswd import CryptModuleNotImportable
from repoze.who.plugins.htpasswd import crypt_check
self.assertEqual(crypt_check('password', hashed), True)
self.assertEqual(crypt_check('notpassword', hashed), False)

with pytest.raises(CryptModuleNotImportable):
crypt_check('password', 'hashed')

def test_sha1_check_w_password_str(self):
from base64 import standard_b64encode
Expand Down
Loading