diff --git a/pyproject.toml b/pyproject.toml index 18bc4d7..c4edb2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ classifiers = [ ] requires-python = ">= 3.8" dependencies = [ - "bcrypt", "humanize", 'importlib-metadata; python_version < "3.10"', "importlib_resources ; python_version < '3.9'", @@ -40,7 +39,7 @@ dependencies = [ [project.optional-dependencies] -db = ["SQLAlchemy", "alembic", "alembic-postgresql-enum"] +db = ["SQLAlchemy", "alembic", "alembic-postgresql-enum", "passlib"] docs = ["Sphinx", "sphinxcontrib-programoutput", "enum-tools[sphinx]", "furo"] tests = ["pylint", "pytest", "pytest-cov", "tox"] diff --git a/src/wuttjamaican/auth.py b/src/wuttjamaican/auth.py index bd63f23..ac6dbe6 100644 --- a/src/wuttjamaican/auth.py +++ b/src/wuttjamaican/auth.py @@ -29,11 +29,18 @@ This defines the default :term:`auth handler`. import secrets import uuid as _uuid -import bcrypt - from wuttjamaican.app import GenericHandler +# nb. this only works if passlib is installed (part of 'db' extra) +try: + from passlib.context import CryptContext +except ImportError: # pragma: no cover + pass +else: + password_context = CryptContext(schemes=["bcrypt"]) + + class AuthHandler(GenericHandler): # pylint: disable=too-many-public-methods """ Base class and default implementation for the :term:`auth @@ -136,7 +143,7 @@ class AuthHandler(GenericHandler): # pylint: disable=too-many-public-methods :returns: ``True`` if password matches; else ``False``. """ - return bcrypt.checkpw(password.encode("utf-8"), user.password.encode("utf-8")) + return password_context.verify(password, user.password) def get_role(self, session, key): """ @@ -412,9 +419,7 @@ class AuthHandler(GenericHandler): # pylint: disable=too-many-public-methods :param password: New password in plain text. """ - user.password = bcrypt.hashpw( - password.encode("utf-8"), bcrypt.gensalt() - ).decode("utf-8") + user.password = password_context.hash(password) def get_role_administrator(self, session): """ diff --git a/tests/test_app.py b/tests/test_app.py index 2a0c89e..1b0e19c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -415,8 +415,8 @@ app_title = WuttaTest self.assertEqual(ver, version("SQLAlchemy")) # can also specify the dist - ver = self.app.get_version(dist="progress") - self.assertEqual(ver, version("progress")) + ver = self.app.get_version(dist="passlib") + self.assertEqual(ver, version("passlib")) def test_make_title(self): text = self.app.make_title("foo_bar") diff --git a/tests/test_conf.py b/tests/test_conf.py index c978e22..684387e 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -8,6 +8,9 @@ from unittest.mock import patch, MagicMock import pytest from wuttjamaican import conf as mod + +# TODO: get rid of this eventually +from wuttjamaican import conf from wuttjamaican.exc import ConfigurationError from wuttjamaican.app import AppHandler from wuttjamaican.testing import FileTestCase, ConfigTestCase @@ -18,19 +21,19 @@ class TestWuttaConfig(FileTestCase): return mod.WuttaConfig(**kwargs) def test_contstructor_basic(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertEqual(config.appname, "wutta") self.assertEqual(config.files_read, []) def test_constructor_valid_files(self): myfile = self.write_file("my.conf", "") - config = mod.WuttaConfig(files=[myfile]) + config = conf.WuttaConfig(files=[myfile]) self.assertEqual(len(config.files_read), 1) self.assertEqual(config.files_read[0], myfile) def test_constructor_missing_files(self): invalid = os.path.join(self.tempdir, "invalid.conf") - self.assertRaises(FileNotFoundError, mod.WuttaConfig, files=[invalid]) + self.assertRaises(FileNotFoundError, conf.WuttaConfig, files=[invalid]) def test_constructor_required_files_are_present(self): first = self.write_file( @@ -53,7 +56,7 @@ baz = B """, ) - config = mod.WuttaConfig(files=[second]) + config = conf.WuttaConfig(files=[second]) self.assertEqual(len(config.files_read), 2) # nb. files_read listing is in order of "priority" which is # same the as order in which files were initially read @@ -74,7 +77,7 @@ baz = B """, ) - self.assertRaises(FileNotFoundError, mod.WuttaConfig, files=[second]) + self.assertRaises(FileNotFoundError, conf.WuttaConfig, files=[second]) def test_constructor_included_files_are_present(self): first = self.write_file( @@ -97,7 +100,7 @@ baz = B """, ) - config = mod.WuttaConfig(files=[second]) + config = conf.WuttaConfig(files=[second]) self.assertEqual(len(config.files_read), 2) # nb. files_read listing is in order of "priority" which is # same the as order in which files were initially read @@ -118,7 +121,7 @@ baz = B """, ) - config = mod.WuttaConfig(files=[second]) + config = conf.WuttaConfig(files=[second]) self.assertEqual(len(config.files_read), 1) self.assertEqual(config.files_read[0], second) self.assertIsNone(config.get("foo.bar")) @@ -156,7 +159,7 @@ baz = C """, ) - config = mod.WuttaConfig(files=[top, middle, base]) + config = conf.WuttaConfig(files=[top, middle, base]) self.assertEqual(len(config.files_read), 3) # nb. files_read listing is in order of "priority" which is # same the as order in which files were initially read @@ -183,7 +186,7 @@ require = %(here)s/first.conf """, ) - config = mod.WuttaConfig(files=[second]) + config = conf.WuttaConfig(files=[second]) files = config.get_prioritized_files() self.assertEqual(len(files), 2) self.assertEqual(files[0], second) @@ -199,16 +202,16 @@ baz = %(__file__)s """, ) - config = mod.WuttaConfig(files=[myconf]) + config = conf.WuttaConfig(files=[myconf]) self.assertEqual(config.get("foo.bar"), f"{self.tempdir}/bar.txt") self.assertEqual(config.get("foo.baz"), myconf) def test_constructor_defaults(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertEqual(config.defaults, {}) self.assertIsNone(config.get("foo")) - config = mod.WuttaConfig(defaults={"foo": "bar"}) + config = conf.WuttaConfig(defaults={"foo": "bar"}) self.assertEqual(config.defaults, {"foo": "bar"}) self.assertEqual(config.get("foo"), "bar") @@ -230,17 +233,17 @@ preferdb = true ) # flags are off by default - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertFalse(config.usedb) self.assertFalse(config.preferdb) # but may override via constructor - config = mod.WuttaConfig(usedb=True, preferdb=True) + config = conf.WuttaConfig(usedb=True, preferdb=True) self.assertTrue(config.usedb) self.assertTrue(config.preferdb) # and also may override via config file - config = mod.WuttaConfig(files=[myfile]) + config = conf.WuttaConfig(files=[myfile]) self.assertTrue(config.usedb) self.assertTrue(config.preferdb) @@ -253,12 +256,12 @@ preferdb = true pytest.skip("test is not relevant without sqlalchemy") # flags are off by default - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertFalse(config.usedb) self.assertFalse(config.preferdb) # but caller may enable the flags (if sqlalchemy available) - config = mod.WuttaConfig(usedb=True, preferdb=True) + config = conf.WuttaConfig(usedb=True, preferdb=True) self.assertTrue(config.usedb) self.assertTrue(config.preferdb) @@ -273,7 +276,7 @@ preferdb = true return orig_import(name, *args, **kwargs) with patch("builtins.__import__", side_effect=mock_import): - config = mod.WuttaConfig(usedb=True, preferdb=True) + config = conf.WuttaConfig(usedb=True, preferdb=True) self.assertFalse(config.usedb) self.assertFalse(config.preferdb) @@ -286,19 +289,19 @@ configure_logging = true """, ) - with patch.object(mod.WuttaConfig, "_configure_logging") as method: + with patch.object(conf.WuttaConfig, "_configure_logging") as method: # no logging config by default - config = mod.WuttaConfig() + config = conf.WuttaConfig() method.assert_not_called() # but may override via constructor method.reset_mock() - config = mod.WuttaConfig(configure_logging=True) + config = conf.WuttaConfig(configure_logging=True) method.assert_called_once() # and also may override via config file method.reset_mock() - config = mod.WuttaConfig(files=[myfile]) + config = conf.WuttaConfig(files=[myfile]) method.assert_called_once() def test_constructor_configures_logging(self): @@ -315,7 +318,7 @@ configure_logging = true with patch("wuttjamaican.conf.logging") as logging: # basic constructor attempts logging config - config = mod.WuttaConfig(configure_logging=True) + config = conf.WuttaConfig(configure_logging=True) logging.config.fileConfig.assert_called_once() # if logging config fails, error is *not* raised @@ -323,18 +326,18 @@ configure_logging = true logging.config.fileConfig.side_effect = configparser.NoSectionError( "logging" ) - config = mod.WuttaConfig(configure_logging=True) + config = conf.WuttaConfig(configure_logging=True) logging.config.fileConfig.assert_called_once() # and it works if we specify config file logging.config.fileConfig.reset_mock() - config = mod.WuttaConfig(files=[myfile]) + config = conf.WuttaConfig(files=[myfile]) logging.config.fileConfig.assert_called_once() def test_config_has_no_app_after_init(self): # initial config should *not* have an app yet, otherwise # extensions cannot specify a default app handler - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertIsNone(config._app) # but after that we can get an app okay @@ -343,7 +346,7 @@ configure_logging = true self.assertIs(app, config._app) def test_setdefault(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() # value is empty by default self.assertIsNone(config.get("foo")) @@ -357,15 +360,15 @@ configure_logging = true self.assertEqual(config.setdefault("baz", "blarg"), "blarg") def test_get_require_with_default(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertRaises(ValueError, config.get, "foo", require=True, default="bar") def test_get_require_missing(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertRaises(ConfigurationError, config.get, "foo", require=True) def test_get_with_default(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() # nb. returns None if no default specified self.assertIsNone(config.get("foo")) self.assertEqual(config.get("foo", default="bar"), "bar") @@ -378,7 +381,7 @@ configure_logging = true pytest.skip("test is not relevant without sqlalchemy") # minimal config, but at least it needs db cxn info - config = mod.WuttaConfig(defaults={"wutta.db.default.url": "sqlite://"}) + config = conf.WuttaConfig(defaults={"wutta.db.default.url": "sqlite://"}) session = Session() @@ -411,17 +414,17 @@ configure_logging = true session.close() def test_get_default(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertIsNone(config.get("foo")) self.assertEqual(config.get("foo", default="bar"), "bar") def test_get_require(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertIsNone(config.get("foo")) self.assertRaises(ConfigurationError, config.get, "foo", require=True) def test_get_require_message(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertIsNone(config.get("foo")) try: config.get("foo", require=True, message="makin stuff up") @@ -436,7 +439,7 @@ configure_logging = true pytest.skip("test is not relevant without sqlalchemy") # start out with a default value - config = mod.WuttaConfig( + config = conf.WuttaConfig( defaults={"wutta.db.default.url": "sqlite://", "foo": "bar"} ) self.assertEqual(config.get("foo"), "bar") @@ -476,7 +479,7 @@ configure_logging = true session.close() def test_get_ambiguous(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() # value is returned if key is not ambiguous config.setdefault("foo", "bar") @@ -487,23 +490,23 @@ configure_logging = true self.assertIsNone(config.get("foo")) def test_require(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertRaises(ConfigurationError, config.require, "foo") def test_get_bool(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertFalse(config.get_bool("foo.bar")) config.setdefault("foo.bar", "true") self.assertTrue(config.get_bool("foo.bar")) def test_get_int(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertIsNone(config.get_int("foo.bar")) config.setdefault("foo.bar", "42") self.assertEqual(config.get_int("foo.bar"), 42) def test_get_list(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertIsNone(config.get_list("foo.bar")) config.setdefault("foo.bar", "hello world") self.assertEqual(config.get_list("foo.bar"), ["hello", "world"]) @@ -623,7 +626,7 @@ configure_logging = true def test_get_app(self): # default handler - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertEqual(config.default_app_handler_spec, "wuttjamaican.app:AppHandler") app = config.get_app() self.assertIsInstance(app, AppHandler) @@ -631,7 +634,7 @@ configure_logging = true self.assertIs(type(app), AppHandler) # custom default handler - config = mod.WuttaConfig() + config = conf.WuttaConfig() config.default_app_handler_spec = "tests.test_conf:CustomAppHandler" app = config.get_app() self.assertIsInstance(app, CustomAppHandler) @@ -643,7 +646,7 @@ configure_logging = true pytest.skip("test is not relevant without sqlalchemy") # default func - config = mod.WuttaConfig() + config = conf.WuttaConfig() self.assertEqual( config.default_engine_maker_spec, "wuttjamaican.db.conf:make_engine_from_config", @@ -652,7 +655,7 @@ configure_logging = true self.assertIs(make_engine, make_engine_from_config) # custom default func - config = mod.WuttaConfig() + config = conf.WuttaConfig() config.default_engine_maker_spec = ( "tests.test_conf:custom_make_engine_from_config" ) @@ -660,7 +663,7 @@ configure_logging = true self.assertIs(make_engine, custom_make_engine_from_config) def test_production(self): - config = mod.WuttaConfig() + config = conf.WuttaConfig() # false if not defined self.assertFalse(config.production()) @@ -680,15 +683,15 @@ def custom_make_engine_from_config(): class TestWuttaConfigExtension(TestCase): def test_basic(self): - config = mod.WuttaConfig() - ext = mod.WuttaConfigExtension() + config = conf.WuttaConfig() + ext = conf.WuttaConfigExtension() self.assertIsNone(ext.key) self.assertEqual(repr(ext), "WuttaConfigExtension(key=None)") class TestGenericDefaultFiles(TestCase): def test_linux(self): - files = mod.generic_default_files("wuttatest") + files = conf.generic_default_files("wuttatest") self.assertIsInstance(files, list) self.assertTrue(len(files) > 1) self.assertIn("/etc/wuttatest.conf", files) @@ -698,7 +701,7 @@ class TestGenericDefaultFiles(TestCase): win32com.shell.SHGetSpecialFolderPath.return_value = r"C:" + os.sep with patch.dict("sys.modules", **{"win32com.shell": win32com}): with patch("wuttjamaican.conf.sys", platform="win32"): - files = mod.generic_default_files("wuttatest") + files = conf.generic_default_files("wuttatest") self.assertIsInstance(files, list) self.assertTrue(len(files) > 1) self.assertIn(os.path.join("C:", "wuttatest.conf"), files) @@ -713,7 +716,7 @@ class TestGenericDefaultFiles(TestCase): with patch("builtins.__import__", side_effect=mock_import): with patch("wuttjamaican.conf.sys", platform="win32"): - files = mod.generic_default_files("wuttatest") + files = conf.generic_default_files("wuttatest") self.assertIsInstance(files, list) self.assertEqual(len(files), 0) @@ -728,11 +731,11 @@ winsvc.RattailFileMonitor = /path/to/other/file """, ) - files = mod.get_config_paths(files=[myconf], winsvc="RattailFileMonitor") + files = conf.get_config_paths(files=[myconf], winsvc="RattailFileMonitor") self.assertEqual(files, ["/path/to/other/file"]) def test_nonexistent_default_files(self): - files = mod.get_config_paths( + files = conf.get_config_paths( files=None, env_files_name="IGNORE_THIS", default_files=["/this/does/not/exist"], @@ -752,7 +755,7 @@ class TestMakeConfig(FileTestCase): with patch("wuttjamaican.conf.WuttaConfig") as WuttaConfig: # generic files are used if nothing is specified generic_default_files.return_value = [generic] - config = mod.make_config(appname="wuttatest", extend=False) + config = conf.make_config(appname="wuttatest") generic_default_files.assert_called_once_with("wuttatest") WuttaConfig.assert_called_once_with( [generic], appname="wuttatest", usedb=None, preferdb=None @@ -762,7 +765,7 @@ class TestMakeConfig(FileTestCase): generic_default_files.reset_mock() generic_default_files.return_value = [] WuttaConfig.reset_mock() - config = mod.make_config(appname="wuttatest", extend=False) + config = conf.make_config(appname="wuttatest") generic_default_files.assert_called_once_with("wuttatest") WuttaConfig.assert_called_once_with( [], appname="wuttatest", usedb=None, preferdb=None @@ -776,7 +779,7 @@ class TestMakeConfig(FileTestCase): with patch("wuttjamaican.conf.WuttaConfig") as WuttaConfig: # generic defaults are used if nothing specified generic_default_files.return_value = [generic] - config = mod.make_config(appname="wuttatest", extend=False) + config = conf.make_config(appname="wuttatest") generic_default_files.assert_called_once_with("wuttatest") WuttaConfig.assert_called_once_with( [generic], appname="wuttatest", usedb=None, preferdb=None @@ -785,9 +788,7 @@ class TestMakeConfig(FileTestCase): # can specify single default file generic_default_files.reset_mock() WuttaConfig.reset_mock() - config = mod.make_config( - appname="wuttatest", default_files=myfile, extend=False - ) + config = conf.make_config(appname="wuttatest", default_files=myfile) generic_default_files.assert_not_called() WuttaConfig.assert_called_once_with( [myfile], appname="wuttatest", usedb=None, preferdb=None @@ -796,9 +797,7 @@ class TestMakeConfig(FileTestCase): # can specify default files as list generic_default_files.reset_mock() WuttaConfig.reset_mock() - config = mod.make_config( - appname="wuttatest", default_files=[myfile], extend=False - ) + config = conf.make_config(appname="wuttatest", default_files=[myfile]) generic_default_files.assert_not_called() WuttaConfig.assert_called_once_with( [myfile], appname="wuttatest", usedb=None, preferdb=None @@ -807,10 +806,8 @@ class TestMakeConfig(FileTestCase): # can specify default files as callable generic_default_files.reset_mock() WuttaConfig.reset_mock() - config = mod.make_config( - appname="wuttatest", - default_files=lambda appname: [myfile], - extend=False, + config = conf.make_config( + appname="wuttatest", default_files=lambda appname: [myfile] ) generic_default_files.assert_not_called() WuttaConfig.assert_called_once_with( @@ -826,7 +823,7 @@ class TestMakeConfig(FileTestCase): generic_default_files.return_value = [generic] # no plus files by default - config = mod.make_config(appname="wuttatest", extend=False) + config = conf.make_config(appname="wuttatest") generic_default_files.assert_called_once_with("wuttatest") WuttaConfig.assert_called_once_with( [generic], appname="wuttatest", usedb=None, preferdb=None @@ -835,9 +832,7 @@ class TestMakeConfig(FileTestCase): # can specify single plus file generic_default_files.reset_mock() WuttaConfig.reset_mock() - config = mod.make_config( - appname="wuttatest", plus_files=myfile, extend=False - ) + config = conf.make_config(appname="wuttatest", plus_files=myfile) generic_default_files.assert_called_once_with("wuttatest") WuttaConfig.assert_called_once_with( [generic, myfile], appname="wuttatest", usedb=None, preferdb=None @@ -846,9 +841,7 @@ class TestMakeConfig(FileTestCase): # can specify plus files as list generic_default_files.reset_mock() WuttaConfig.reset_mock() - config = mod.make_config( - appname="wuttatest", plus_files=[myfile], extend=False - ) + config = conf.make_config(appname="wuttatest", plus_files=[myfile]) generic_default_files.assert_called_once_with("wuttatest") WuttaConfig.assert_called_once_with( [generic, myfile], appname="wuttatest", usedb=None, preferdb=None @@ -857,10 +850,8 @@ class TestMakeConfig(FileTestCase): # can specify plus files via env generic_default_files.reset_mock() WuttaConfig.reset_mock() - config = mod.make_config( - appname="wuttatest", - env={"WUTTATEST_CONFIG_PLUS_FILES": myfile}, - extend=False, + config = conf.make_config( + appname="wuttatest", env={"WUTTATEST_CONFIG_PLUS_FILES": myfile} ) generic_default_files.assert_called_once_with("wuttatest") WuttaConfig.assert_called_once_with( @@ -876,7 +867,7 @@ class TestMakeConfig(FileTestCase): generic_default_files.return_value = [generic] # generic files by default - config = mod.make_config(appname="wuttatest", extend=False) + config = conf.make_config(appname="wuttatest") generic_default_files.assert_called_once_with("wuttatest") WuttaConfig.assert_called_once_with( [generic], appname="wuttatest", usedb=None, preferdb=None @@ -885,7 +876,7 @@ class TestMakeConfig(FileTestCase): # can specify single primary file (nb. no default files) generic_default_files.reset_mock() WuttaConfig.reset_mock() - config = mod.make_config(myfile, appname="wuttatest", extend=False) + config = conf.make_config(myfile, appname="wuttatest") generic_default_files.assert_not_called() WuttaConfig.assert_called_once_with( [myfile], appname="wuttatest", usedb=None, preferdb=None @@ -894,7 +885,7 @@ class TestMakeConfig(FileTestCase): # can specify primary files as list generic_default_files.reset_mock() WuttaConfig.reset_mock() - config = mod.make_config([myfile], appname="wuttatest", extend=False) + config = conf.make_config([myfile], appname="wuttatest") generic_default_files.assert_not_called() WuttaConfig.assert_called_once_with( [myfile], appname="wuttatest", usedb=None, preferdb=None @@ -903,10 +894,8 @@ class TestMakeConfig(FileTestCase): # can specify primary files via env generic_default_files.reset_mock() WuttaConfig.reset_mock() - config = mod.make_config( - appname="wuttatest", - env={"WUTTATEST_CONFIG_FILES": myfile}, - extend=False, + config = conf.make_config( + appname="wuttatest", env={"WUTTATEST_CONFIG_FILES": myfile} ) generic_default_files.assert_not_called() WuttaConfig.assert_called_once_with(