diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..179c4c6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +## unreleased 0.4.0 + +### Add + + - Added apr1.py module from https://github.com/Tblue/pyapr1 to support + apr1 hash algorithm. + +### Remove + + - Removed passlib dependency since it's been unmaintained for 5 years + and is causing compatibility issue with bcrypt module. + +### Change + + - Used standard library hashlib and crypt to support SHA and CRYPT hash. diff --git a/README.md b/README.md index 66864aa..fdea1e4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,8 @@ # Flat file authentication plugin for StackStorm Community edition -[![Build Status](https://api.travis-ci.org/StackStorm/st2-auth-backend-flat-file.svg?branch=master)](https://travis-ci.org/StackStorm/st2-auth-backend-flat-file) [![IRC](https://img.shields.io/irc/%23stackstorm.png)](http://webchat.freenode.net/?channels=stackstorm) +[![Tox CI](https://github.com/StackStorm/st2-auth-backend-flat-file/actions/workflows/tox.yaml/badge.svg)](https://github.com/StackStorm/st2-auth-backend-flat-file/actions/workflows/tox.yaml) -Flat file backend supports reading credentials from an Apache HTTPd htpasswd formatted file. To -manage this file you can use [htpasswd](https://httpd.apache.org/docs/2.2/programs/htpasswd.html) -utility which comes with a standard Apache httpd distribution or by installing apache2-utils -package on Ubuntu / Debian. +Flat file backend supports reading credentials from an Apache HTTPd htpasswd formatted file. To manage this file you can use [htpasswd](https://httpd.apache.org/docs/2.2/programs/htpasswd.html) utility which comes with a standard Apache httpd distribution or by installing apache2-utils package on Ubuntu / Debian. ### Configuration Options @@ -15,10 +12,7 @@ package on Ubuntu / Debian. ### Configuration Example -Please refer to the authentication section in the StackStorm -[documentation](http://docs.stackstorm.com) for basic setup concept. The -following is an example of the auth section in the StackStorm configuration file for the flat-file -backend. +Please refer to the authentication section in the StackStorm [documentation](http://docs.stackstorm.com) for basic setup concept. The following is an example of the auth section in the StackStorm configuration file for the flat-file backend. ```ini [auth] @@ -34,14 +28,15 @@ api_url = https://myhost.example.com:9101 debug = False ``` -The following is an sample htpasswd command to generate a password file with a user entry. +The following is an example htpasswd command to generate a password file with a user entry. You should be using no other hashing algorithm than *bcrypt* as it is consider the only secure hashing algorithm amoung all the algorithms supported by htpasswd. ``` -htpasswd -cs /path/to/.htpasswd stark +htpasswd -cB /path/to/.htpasswd stark ``` ## Copyright, License, and Contributors Agreement +Copyright 2025 StackStorm, Inc. Copyright 2015 StackStorm, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in diff --git a/dist_utils.py b/dist_utils.py index e817386..88f077c 100644 --- a/dist_utils.py +++ b/dist_utils.py @@ -43,15 +43,15 @@ else: text_type = unicode # NOQA -GET_PIP = 'curl https://bootstrap.pypa.io/get-pip.py | python' +GET_PIP = "curl https://bootstrap.pypa.io/get-pip.py | python" __all__ = [ - 'check_pip_is_installed', - 'check_pip_version', - 'fetch_requirements', - 'apply_vagrant_workaround', - 'get_version_string', - 'parse_version_string' + "check_pip_is_installed", + "check_pip_version", + "fetch_requirements", + "apply_vagrant_workaround", + "get_version_string", + "parse_version_string", ] @@ -62,15 +62,15 @@ def check_pip_is_installed(): try: import pip # NOQA except ImportError as e: - print('Failed to import pip: %s' % (text_type(e))) - print('') - print('Download pip:\n%s' % (GET_PIP)) + print("Failed to import pip: %s" % (text_type(e))) + print("") + print("Download pip:\n%s" % (GET_PIP)) sys.exit(1) return True -def check_pip_version(min_version='6.0.0'): +def check_pip_version(min_version="6.0.0"): """ Ensure that a minimum supported version of pip is installed. """ @@ -79,10 +79,12 @@ def check_pip_version(min_version='6.0.0'): import pip if StrictVersion(pip.__version__) < StrictVersion(min_version): - print("Upgrade pip, your version '{0}' " - "is outdated. Minimum required version is '{1}':\n{2}".format(pip.__version__, - min_version, - GET_PIP)) + print( + "Upgrade pip, your version '{0}' " + "is outdated. Minimum required version is '{1}':\n{2}".format( + pip.__version__, min_version, GET_PIP + ) + ) sys.exit(1) return True @@ -96,30 +98,30 @@ def fetch_requirements(requirements_file_path): reqs = [] def _get_link(line): - vcs_prefixes = ['git+', 'svn+', 'hg+', 'bzr+'] + vcs_prefixes = ["git+", "svn+", "hg+", "bzr+"] for vcs_prefix in vcs_prefixes: - if line.startswith(vcs_prefix) or line.startswith('-e %s' % (vcs_prefix)): - req_name = re.findall('.*#egg=(.+)([&|@]).*$', line) + if line.startswith(vcs_prefix) or line.startswith("-e %s" % (vcs_prefix)): + req_name = re.findall(".*#egg=(.+)([&|@]).*$", line) if not req_name: - req_name = re.findall('.*#egg=(.+?)$', line) + req_name = re.findall(".*#egg=(.+?)$", line) else: req_name = req_name[0] if not req_name: raise ValueError('Line "%s" is missing "#egg="' % (line)) - link = line.replace('-e ', '').strip() + link = line.replace("-e ", "").strip() return link, req_name[0] return None, None - with open(requirements_file_path, 'r') as fp: + with open(requirements_file_path, "r") as fp: for line in fp.readlines(): line = line.strip() - if line.startswith('#') or not line: + if line.startswith("#") or not line: continue link, req_name = _get_link(line=line) @@ -129,8 +131,8 @@ def _get_link(line): else: req_name = line - if ';' in req_name: - req_name = req_name.split(';')[0].strip() + if ";" in req_name: + req_name = req_name.split(";")[0].strip() reqs.append(req_name) @@ -144,7 +146,7 @@ def apply_vagrant_workaround(): Note: Without this workaround, setup.py sdist will fail when running inside a shared directory (nfs / virtualbox shared folders). """ - if os.environ.get('USER', None) == 'vagrant': + if os.environ.get("USER", None) == "vagrant": del os.link @@ -153,14 +155,13 @@ def get_version_string(init_file): Read __version__ string for an init file. """ - with open(init_file, 'r') as fp: + with open(init_file, "r") as fp: content = fp.read() - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", - content, re.M) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", content, re.M) if version_match: return version_match.group(1) - raise RuntimeError('Unable to find version string in %s.' % (init_file)) + raise RuntimeError("Unable to find version string in %s." % (init_file)) # alias for get_version_string diff --git a/lint-configs/python/.pylintrc b/lint-configs/python/.pylintrc index 2ac0894..64d7de7 100644 --- a/lint-configs/python/.pylintrc +++ b/lint-configs/python/.pylintrc @@ -22,3 +22,6 @@ ignored-modules=distutils,eventlet.green.subprocess max-line-length=100 max-module-lines=1000 indent-string=' ' + +[MASTER] +init-hook='import sys; sys.path.append("./st2auth_flat_file_backend")' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3b92ef5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.pytest.ini_options] +pythonpath = [ + ".", + "st2auth_flat_file_backend" +] diff --git a/requirements.txt b/requirements.txt index bd5e796..e5bca66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1 @@ -# passlib can use several libs to provide bcrypt (which is required for htpasswd support), -# but passlib deprecated support for py-bcrypt, a bcrypt lib alternative. -# The [bcrypt] extra ensures we use bcrypt instead of some other lib. -passlib[bcrypt]>=1.7.1,<1.8.0 +bcrypt>=4.3.0 diff --git a/setup.py b/setup.py index ec59996..1032a2d 100644 --- a/setup.py +++ b/setup.py @@ -22,8 +22,8 @@ from dist_utils import parse_version_string BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -REQUIREMENTS_FILE = os.path.join(BASE_DIR, 'requirements.txt') -INIT_FILE = os.path.join(BASE_DIR, 'st2auth_flat_file_backend', '__init__.py') +REQUIREMENTS_FILE = os.path.join(BASE_DIR, "requirements.txt") +INIT_FILE = os.path.join(BASE_DIR, "st2auth_flat_file_backend", "__init__.py") version = parse_version_string(INIT_FILE) install_reqs, dep_links = fetch_requirements(REQUIREMENTS_FILE) @@ -32,40 +32,40 @@ long_description = fh.read() setup( - name='st2-auth-backend-flat-file', + name="st2-auth-backend-flat-file", version=version, - description='StackStorm authentication backend which reads credentials from a htpasswd compatible file on disk.', + description="StackStorm authentication backend which reads credentials from a htpasswd compatible file on disk.", long_description=long_description, long_description_content_type="text/markdown", - author='StackStorm, Inc.', - author_email='info@stackstorm.com', - url='https://github.com/StackStorm/st2-auth-backend-flat-file', - license='Apache License (2.0)', - download_url='https://github.com/StackStorm/st2-auth-backend-flat-file/tarball/master', + author="StackStorm, Inc.", + author_email="info@stackstorm.com", + url="https://github.com/StackStorm/st2-auth-backend-flat-file", + license="Apache License (2.0)", + download_url="https://github.com/StackStorm/st2-auth-backend-flat-file/tarball/master", classifiers=[ - 'Development Status :: 3 - Alpha', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Intended Audience :: Developers', - 'Environment :: Console', + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Intended Audience :: Developers", + "Environment :: Console", ], - python_requires='>=3.8', - platforms=['Any'], + python_requires=">=3.8", + platforms=["Any"], scripts=[], - provides=['st2auth_flat_file_backend'], + provides=["st2auth_flat_file_backend"], packages=find_packages(), include_package_data=True, install_requires=install_reqs, dependency_links=dep_links, entry_points={ - 'st2auth.backends.backend': [ - 'flat_file = st2auth_flat_file_backend.flat_file:FlatFileAuthenticationBackend', + "st2auth.backends.backend": [ + "flat_file = st2auth_flat_file_backend.flat_file:FlatFileAuthenticationBackend", ], }, - zip_safe=False + zip_safe=False, ) diff --git a/st2auth_flat_file_backend/__init__.py b/st2auth_flat_file_backend/__init__.py index a71c03c..b877468 100644 --- a/st2auth_flat_file_backend/__init__.py +++ b/st2auth_flat_file_backend/__init__.py @@ -17,8 +17,6 @@ from .flat_file import FlatFileAuthenticationBackend -__all__ = [ - 'FlatFileAuthenticationBackend' -] +__all__ = ["FlatFileAuthenticationBackend"] -__version__ = '0.3.0' +__version__ = "0.4.0" diff --git a/st2auth_flat_file_backend/apr1.py b/st2auth_flat_file_backend/apr1.py new file mode 100644 index 0000000..32b9109 --- /dev/null +++ b/st2auth_flat_file_backend/apr1.py @@ -0,0 +1,157 @@ +# pyapr1 - A Python implementation of the APR1 algorithm +# +# Copyright (c) 2015, Tilman Blumenbach +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of pyapr1 nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# The to64() and hash_apr1() functions are based on code from the Apache Portable +# Runtime Utility Library (namely, on the two functions to64() and +# apr_md5_encode() from the file crypto/apr_md5.c). The licenses for that original +# material are included below: +# +# ============================================================================ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# +# The apr_md5_encode() routine uses much code obtained from the FreeBSD 3.0 +# MD5 crypt() function, which is licenced as follows: +# ---------------------------------------------------------------------------- +# "THE BEER-WARE LICENSE" (Revision 42): +# wrote this file. As long as you retain this notice you +# can do whatever you want with this stuff. If we meet some day, and you think +# this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp +# ---------------------------------------------------------------------------- +# +# ============================================================================ + +# 20250910 - This file was sourced from https://github.com/Tblue/pyapr1 and is used +# to provide backward compatibility with older htpasswd tools, notably +# apache 2.2 and lower. The apr1 algorithm is not considered secure and +# bcrypt is currently the recommended algorithm. + +import os + +from hashlib import md5 + + +def to64(data, n_out): + chars = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + out = "" + + for i in range(n_out): + out += chars[data & 0x3F] + data >>= 6 + + return out + + +def mkint(data, *indexes): + r = 0 + for i, idx in enumerate(indexes): + r |= data[idx] << 8 * (len(indexes) - i - 1) + + return r + + +def hash_apr1(salt, password): + sb = bytes(salt, "utf-8") + pb = bytes(password, "utf-8") + ph = md5() + + # First, the password. + ph.update(pb) + # Then, the magic string. + ph.update(b"$apr1$") + # Then, the salt. + ph.update(sb) + + # Weird stuff. + sandwich = md5(pb + sb + pb).digest() + ndig, nrem = divmod(len(pb), ph.digest_size) + for n in ndig * [ph.digest_size] + [nrem]: + ph.update(sandwich[:n]) + + # Even more weird stuff. + i = len(pb) + while i: + if i & 1: + ph.update(b"\0") + else: + ph.update(pb[:1]) + + i >>= 1 + + final = ph.digest() + for i in range(1000): + maelstrom = md5() + + if i & 1: + maelstrom.update(pb) + else: + maelstrom.update(final) + + if i % 3: + maelstrom.update(sb) + + if i % 7: + maelstrom.update(pb) + + if i & 1: + maelstrom.update(final) + else: + maelstrom.update(pb) + + final = maelstrom.digest() + + pw_ascii = ( + to64(mkint(final, 0, 6, 12), 4) + + to64(mkint(final, 1, 7, 13), 4) # noqa: W503 + + to64(mkint(final, 2, 8, 14), 4) # noqa: W503 + + to64(mkint(final, 3, 9, 15), 4) # noqa: W503 + + to64(mkint(final, 4, 10, 5), 4) # noqa: W503 + + to64(mkint(final, 11), 2) # noqa: W503 + ) + return "$apr1$%s$%s" % (salt, pw_ascii) + + +def generate_salt(): + return to64(mkint(os.urandom(6), *range(6)), 8) diff --git a/st2auth_flat_file_backend/flat_file.py b/st2auth_flat_file_backend/flat_file.py index fd70735..a2e85a1 100644 --- a/st2auth_flat_file_backend/flat_file.py +++ b/st2auth_flat_file_backend/flat_file.py @@ -14,39 +14,97 @@ # limitations under the License. import logging - -from passlib.apache import HtpasswdFile - -__all__ = [ - 'FlatFileAuthenticationBackend' -] +import locale +import bcrypt +import base64 +import hashlib +import apr1 +import crypt # deprecated in 3.11 and removed in 3.13. +from hmac import compare_digest as compare_hash + +# Reference: https://httpd.apache.org/docs/2.4/misc/password_encryptions.html +# https://akkadia.org/drepper/SHA-crypt.txt + +# empirical format for htpasswd using apache utils 2.4.41 +# md5sum:$apr1$AmfEURVX$U0A7kxYcofNn2J.lptuOn0 +# bcrypt:$2y$05$LtdiiELPayMNfwk5PMWA2uOMNAWW9wacCrYgN.lXUR35YEG.kOPWO +# sha:{SHA}C5wmJdwh7wX2rU3fR8XyA4N6oyw= +# crypt:znExVsGU19vAQ +# plain:toto + +__all__ = ["FlatFileAuthenticationBackend"] LOG = logging.getLogger(__name__) -COMMENT_MARKER = '**COMMENTIGNORE**' - -class HttpasswdFileWithComments(HtpasswdFile): +class HtpasswdFile(object): """ - Custom HtpasswdFile implementation which supports comments (lines starting - with #). + Custom HtpasswdFile implementation which supports comments + (lines starting with #). """ - def _load_lines(self, lines): - super(HttpasswdFileWithComments, self)._load_lines(lines=lines) - - # Filter out comments - self._records.pop(COMMENT_MARKER, None) - assert COMMENT_MARKER not in self._records + def __init__(self, filename): + self.filename = filename + self.entries = {} + self._load_file() - def _parse_record(self, record, lineno): - if record.startswith(b'#'): - # Comment, add special marker so we can filter it out later - return (COMMENT_MARKER, None) + def _load_file(self): + """ + Load apache htpasswd formatted file with support for lines starting with "#" + as comments. The format is a single line per record as : - result = super(HttpasswdFileWithComments, self)._parse_record(record=record, - lineno=lineno) - return result + Records are added to the 'entries' dictionary with the username as the key + and hash data as the value. + """ + data = None + with open(self.filename, "r") as f: + data = f.readlines() + for line in data: + line = line.strip() + if line.startswith("#"): + LOG.debug(f"Skip comment {line}") + continue + if ":" not in line: + LOG.debug(f"Malformed entry '{line}'.") + continue + username, hash_data = line.split(":", 1) + self.entries[username] = hash_data + + def check_password(self, username, password): + if username in self.entries: + hash_data = self.entries[username] + encode_local = locale.getpreferredencoding() + pw = bytes(password, encoding=encode_local) + if hash_data.startswith("$apr1$"): + LOG.warning( + "%s uses MD5 algorithm to hash the password." + "Rehash the password with bcrypt is strongly recommended.", + username, + ) + _, _, salt, md5hash = hash_data.split("$") + return apr1.hash_apr1(salt, password) == hash_data + elif hash_data.startswith("$2y$"): + return bcrypt.checkpw(pw, bytes(hash_data, encoding=encode_local)) + elif hash_data.startswith("{SHA}"): + LOG.warning( + "%s uses deprecated SHA algorithm to hash password." + "Rehash the password with bcrypt.", + username, + ) + return bytes(hash_data, encoding=encode_local) == b"{SHA}" + base64.b64encode( + hashlib.sha1(pw).digest() + ) + else: + # crypt is deprecated and will be dropped in python 3.13. + LOG.warning( + "%s uses deprecated crypt algorithm to hash password." + "Rehash the password with bcrypt.", + username, + ) + return compare_hash(crypt.crypt(password, hash_data), hash_data) + else: + # User not found. + return None class FlatFileAuthenticationBackend(object): @@ -56,8 +114,6 @@ class FlatFileAuthenticationBackend(object): Entries need to be in a htpasswd file like format. This means entries can be managed with the htpasswd utility (https://httpd.apache.org/docs/current/programs/htpasswd.html) which ships with Apache HTTP server. - - Note: This backends depends on the "passlib" library. """ def __init__(self, file_path): @@ -68,7 +124,7 @@ def __init__(self, file_path): self._file_path = file_path def authenticate(self, username, password): - htpasswd_file = HttpasswdFileWithComments(path=self._file_path) + htpasswd_file = HtpasswdFile(self._file_path) result = htpasswd_file.check_password(username, password) if result is None: diff --git a/tests/unit/test_flat_file_backend.py b/tests/unit/test_flat_file_backend.py index 4907481..6e96914 100644 --- a/tests/unit/test_flat_file_backend.py +++ b/tests/unit/test_flat_file_backend.py @@ -25,48 +25,49 @@ class FlatFileAuthenticationBackendTestCase(unittest.TestCase): def test_authenticate_httpasswd_file_without_comments(self): - file_path = os.path.join(BASE_DIR, '../fixtures/htpasswd_test') + file_path = os.path.join(BASE_DIR, "../fixtures/htpasswd_test") backend = FlatFileAuthenticationBackend(file_path=file_path) # Inexistent user - self.assertFalse(backend.authenticate(username='doesntexist', password='bar')) + self.assertFalse(backend.authenticate(username="doesntexist", password="bar")) # Invalid password - self.assertFalse(backend.authenticate(username='test1', password='bar')) + self.assertFalse(backend.authenticate(username="test1", password="bar")) # Valid password (md5 hash) - self.assertTrue(backend.authenticate(username='test1', password='testpassword')) + self.assertTrue(backend.authenticate(username="test1", password="testpassword")) # Valid password (sha hash - insecure) - self.assertTrue(backend.authenticate(username='test3', password='testpassword')) + self.assertTrue(backend.authenticate(username="test3", password="testpassword")) # Valid password (crypt - insecure) - self.assertTrue(backend.authenticate(username='test4', password='testpassword')) + self.assertTrue(backend.authenticate(username="test4", password="testpassword")) def test_authenticate_httpasswd_file_with_comments(self): - file_path = os.path.join(BASE_DIR, '../fixtures/htpasswd_test_with_comments') + file_path = os.path.join(BASE_DIR, "../fixtures/htpasswd_test_with_comments") backend = FlatFileAuthenticationBackend(file_path=file_path) # Inexistent user - self.assertFalse(backend.authenticate(username='doesntexist', password='bar')) + self.assertFalse(backend.authenticate(username="doesntexist", password="bar")) # Invalid password - self.assertFalse(backend.authenticate(username='test1', password='bar')) + self.assertFalse(backend.authenticate(username="test1", password="bar")) # Valid password (md5 hash) - self.assertTrue(backend.authenticate(username='test1', password='testpassword')) + self.assertTrue(backend.authenticate(username="test1", password="testpassword")) # Valid password (sha hash - insecure) - self.assertTrue(backend.authenticate(username='test3', password='testpassword')) + self.assertTrue(backend.authenticate(username="test3", password="testpassword")) # Valid password (crypt - insecure) - self.assertTrue(backend.authenticate(username='test4', password='testpassword')) + self.assertTrue(backend.authenticate(username="test4", password="testpassword")) def test_authenticate_httpasswd_file_doesnt_exist(self): - file_path = os.path.join(BASE_DIR, '../fixtures/htpasswd_doesnt_exist') + file_path = os.path.join(BASE_DIR, "../fixtures/htpasswd_doesnt_exist") backend = FlatFileAuthenticationBackend(file_path=file_path) - self.assertRaises(IOError, backend.authenticate, username='doesntexist', password='bar') + self.assertRaises(IOError, backend.authenticate, username="doesntexist", password="bar") -if __name__ == '__main__': + +if __name__ == "__main__": sys.exit(unittest.main()) diff --git a/tox.ini b/tox.ini index cabb9b6..2596eb2 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py38,py39,py310,py311,lint [testenv] deps = -r{toxinidir}/test-requirements.txt -commands = pytest +commands = python -m pytest tests [testenv:py38] basepython = python3.8