From ae8071c5bc440c2b45e02b4bdb587ec81f0330ef5d9c7a6c8c92d7b209394ec2 Mon Sep 17 00:00:00 2001 From: Steve Kowalik Date: Fri, 30 Sep 2022 06:53:24 +0000 Subject: [PATCH] - Upate to 5.0.2: * Role permissions backwards compatibility bug. * Fix Change Password regression. * Support for WebAuthn. * Support Two-factor recovery codes. * Provide option to prevent user enumeration (i.e. Generic Responses). * Support for Python 3.10. * Support for Flask >= 2.2. * Add custom HTML attributes to improve user experience. * Make the required zxcvbn complexity score configurable. * Get rid of Flask-Mail. Flask-Mailman is now the default preferred email package. * A delete option has been added to us-setup (form and view). * Improve username support - the LoginForm now has a separate field for username. * Fix test and other failures with newer Flask-Login/Werkzeug versions. * Fix test failures with newer Flask versions. - Drop patch endswith-assert.patch: * Included upstream. - Rebase patches no-mongodb.patch and use-pyqrcodeng.patch - Update {Build,}Requires versions. OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:flask/python-Flask-Security-Too?expand=0&rev=23 --- Flask-Security-Too-4.1.3.tar.gz | 3 - Flask-Security-Too-5.0.2.tar.gz | 3 + endswith-assert.patch | 260 ------------------------------ no-mongodb.patch | 14 +- python-Flask-Security-Too.changes | 25 +++ python-Flask-Security-Too.spec | 33 ++-- use-pyqrcodeng.patch | 33 ++-- 7 files changed, 71 insertions(+), 300 deletions(-) delete mode 100644 Flask-Security-Too-4.1.3.tar.gz create mode 100644 Flask-Security-Too-5.0.2.tar.gz delete mode 100644 endswith-assert.patch diff --git a/Flask-Security-Too-4.1.3.tar.gz b/Flask-Security-Too-4.1.3.tar.gz deleted file mode 100644 index b017665..0000000 --- a/Flask-Security-Too-4.1.3.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:996d8d286789a72478462ee6f8a1371254a0dc4f8feea6f33534c2a3772c91cf -size 446589 diff --git a/Flask-Security-Too-5.0.2.tar.gz b/Flask-Security-Too-5.0.2.tar.gz new file mode 100644 index 0000000..75c6033 --- /dev/null +++ b/Flask-Security-Too-5.0.2.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36fee0da5d1b3d211caf274553b7753478c208997c624abb84ebba4261de65c2 +size 556637 diff --git a/endswith-assert.patch b/endswith-assert.patch deleted file mode 100644 index 64b6486..0000000 --- a/endswith-assert.patch +++ /dev/null @@ -1,260 +0,0 @@ ---- - tests/test_changeable.py | 4 ++-- - tests/test_common.py | 4 ++-- - tests/test_configuration.py | 4 ++-- - tests/test_confirmable.py | 2 +- - tests/test_misc.py | 23 +++++++---------------- - tests/test_recoverable.py | 2 +- - tests/test_response.py | 7 ++----- - tests/test_two_factor.py | 10 ++++------ - tests/test_unified_signin.py | 11 ++++------- - 9 files changed, 25 insertions(+), 42 deletions(-) - ---- a/tests/test_changeable.py -+++ b/tests/test_changeable.py -@@ -193,7 +193,7 @@ def test_change_invalidates_session(app, - # try to access protected endpoint - shouldn't work - response = client.get("/profile") - assert response.status_code == 302 -- assert response.headers["Location"] == "http://localhost/login?next=%2Fprofile" -+ assert response.headers["Location"].endswith("/login?next=%2Fprofile") - - - def test_change_updates_remember(app, client): -@@ -243,7 +243,7 @@ def test_change_invalidates_auth_token(a - # authtoken should now be invalid - response = client.get("/token", headers=headers) - assert response.status_code == 302 -- assert response.headers["Location"] == "http://localhost/login?next=%2Ftoken" -+ assert response.headers["Location"].endswith("/login?next=%2Ftoken") - - - def test_auth_uniquifier(app): ---- a/tests/test_common.py -+++ b/tests/test_common.py -@@ -324,7 +324,7 @@ def test_unauthorized_access_with_referr - response = client.get( - "/admin?a=b", headers={"referer": "http://localhost/admin?x=y"} - ) -- assert response.headers["Location"] == "http://localhost/" -+ assert response.headers["Location"].endswith("/") - client.get(response.headers["Location"]) - - response = client.get( -@@ -336,7 +336,7 @@ def test_unauthorized_access_with_referr - # we expect a temp redirect (302) to the referer - response = client.get("/admin?w=s", headers={"referer": "/profile"}) - assert response.status_code == 302 -- assert response.headers["Location"] == "http://localhost/profile" -+ assert response.headers["Location"].endswith("/profile") - - - @pytest.mark.settings(unauthorized_view="/unauthz") ---- a/tests/test_configuration.py -+++ b/tests/test_configuration.py -@@ -24,11 +24,11 @@ def test_view_configuration(client): - - response = authenticate(client, endpoint="/custom_login") - assert "location" in response.headers -- assert response.headers["Location"] == "http://localhost/post_login" -+ assert response.headers["Location"].endswith("/post_login") - - response = logout(client, endpoint="/custom_logout") - assert "location" in response.headers -- assert response.headers["Location"] == "http://localhost/post_logout" -+ assert response.headers["Location"].endswith("/post_logout") - - response = client.get( - "/http", ---- a/tests/test_confirmable.py -+++ b/tests/test_confirmable.py -@@ -522,7 +522,7 @@ def test_email_not_identity(app, sqlalch - token = registrations[0]["confirm_token"] - response = client.get("/confirm/" + token, headers={"Accept": "application/json"}) - assert response.status_code == 302 -- assert response.location == "http://localhost/" -+ assert response.location.endswith("/") - - logout(client) - ---- a/tests/test_misc.py -+++ b/tests/test_misc.py -@@ -813,10 +813,7 @@ def test_authn_freshness( - with capture_flashes() as flashes: - response = client.get("/myspecialview", follow_redirects=False) - assert response.status_code == 302 -- assert ( -- response.location -- == "http://localhost/verify?next=http%3A%2F%2Flocalhost%2Fmyspecialview" -- ) -+ assert response.location.endswith("/verify?next=http%3A%2F%2Flocalhost%2Fmyspecialview") - assert flashes[0]["category"] == "error" - assert flashes[0]["message"].encode("utf-8") == get_message( - "REAUTHENTICATION_REQUIRED" -@@ -896,10 +893,7 @@ def test_default_authn_bp(app, client): - time.sleep(0.1) - response = client.get("/myview", follow_redirects=False) - assert response.status_code == 302 -- assert ( -- response.location -- == "http://localhost/myprefix/verify?next=http%3A%2F%2Flocalhost%2Fmyview" -- ) -+ assert response.location.endswith("/myprefix/verify?next=http%3A%2F%2Flocalhost%2Fmyview") - - - def test_authn_freshness_grace(app, client, get_message): -@@ -941,10 +935,7 @@ def test_authn_freshness_nc(app, client_ - # This should fail - should be a redirect - response = client_nc.get("/myview", headers=h, follow_redirects=False) - assert response.status_code == 302 -- assert ( -- response.location -- == "http://localhost/verify?next=http%3A%2F%2Flocalhost%2Fmyview" -- ) -+ assert response.location.endswith("/verify?next=http%3A%2F%2Flocalhost%2Fmyview") - - - def test_verify_fresh(app, client, get_message): -@@ -1106,11 +1097,11 @@ def test_post_security_with_application_ - "/login", data=dict(email="matt@lp.com", password="password") - ) - assert response.status_code == 302 -- assert response.headers["Location"] == "http://localhost/root" -+ assert response.headers["Location"].endswith("/root") - - response = client.get("/logout") - assert response.status_code == 302 -- assert response.headers["Location"] == "http://localhost/root" -+ assert response.headers["Location"].endswith("/root") - - - def test_post_security_with_application_root_and_views(app, sqlalchemy_datastore): -@@ -1129,11 +1120,11 @@ def test_post_security_with_application_ - "/login", data=dict(email="matt@lp.com", password="password") - ) - assert response.status_code == 302 -- assert response.headers["Location"] == "http://localhost/post_login" -+ assert response.headers["Location"].endswith("/post_login") - - response = client.get("/logout") - assert response.status_code == 302 -- assert response.headers["Location"] == "http://localhost/post_logout" -+ assert response.headers["Location"].endswith("/post_logout") - - - @pytest.mark.settings(redirect_validate_mode="regex") ---- a/tests/test_recoverable.py -+++ b/tests/test_recoverable.py -@@ -289,7 +289,7 @@ def test_recover_invalidates_session(app - # try to access protected endpoint with old session - shouldn't work - response = other_client.get("/profile") - assert response.status_code == 302 -- assert response.headers["Location"] == "http://localhost/login?next=%2Fprofile" -+ assert response.headers["Location"].endswith("/login?next=%2Fprofile") - - - def test_login_form_description(sqlalchemy_app): ---- a/tests/test_response.py -+++ b/tests/test_response.py -@@ -52,7 +52,7 @@ def test_default_unauthn(app, client): - - response = client.get("/profile") - assert response.status_code == 302 -- assert response.headers["Location"] == "http://localhost/login?next=%2Fprofile" -+ assert response.headers["Location"].endswith("/login?next=%2Fprofile") - - response = client.get("/profile", headers={"Accept": "application/json"}) - assert response.status_code == 401 -@@ -68,10 +68,7 @@ def test_default_unauthn_bp(app, client) - - response = client.get("/profile") - assert response.status_code == 302 -- assert ( -- response.headers["Location"] -- == "http://localhost/myprefix/mylogin?next=%2Fprofile" -- ) -+ assert response.headers["Location"].endswith("/myprefix/mylogin?next=%2Fprofile") - - - def test_default_unauthn_myjson(app, client): ---- a/tests/test_two_factor.py -+++ b/tests/test_two_factor.py -@@ -851,7 +851,7 @@ def test_admin_setup_reset(app, client, - # we shouldn't be logged in - response = client.get("/profile", follow_redirects=False) - assert response.status_code == 302 -- assert response.location == "http://localhost/login?next=%2Fprofile" -+ assert response.location.endswith("/login?next=%2Fprofile") - - # Use admin to setup gene's SMS/phone. - with app.app_context(): -@@ -1105,7 +1105,7 @@ def test_bad_sender(app, client, get_mes - data = {"email": "gal@lp.com", "password": "password"} - response = client.post("login", data=data, follow_redirects=False) - assert response.status_code == 302 -- assert response.location == "http://localhost/login" -+ assert response.location.endswith("/login") - assert get_message("FAILED_TO_SEND_CODE") in flashes[0]["message"].encode("utf-8") - - # test w/ JSON -@@ -1187,9 +1187,7 @@ def test_verify(app, client, get_message - authenticate(client) - response = client.get("tf-setup", follow_redirects=False) - verify_url = response.location -- assert ( -- verify_url == "http://localhost/verify?next=http%3A%2F%2Flocalhost%2Ftf-setup" -- ) -+ assert verify_url.endswith("/verify?next=http%3A%2F%2Flocalhost%2Ftf-setup") - logout(client) - - # Now try again - follow redirects to get to verify form -@@ -1220,7 +1218,7 @@ def test_verify(app, client, get_message - follow_redirects=False, - ) - assert response.status_code == 302 -- assert response.location == "http://localhost/tf-setup" -+ assert response.location.endswith("/tf-setup") - assert get_message("REAUTHENTICATION_SUCCESSFUL") == flashes[0]["message"].encode( - "utf-8" - ) ---- a/tests/test_unified_signin.py -+++ b/tests/test_unified_signin.py -@@ -513,7 +513,7 @@ def test_verify_link(app, client, get_me - - # Try with no code - response = client.get("us-verify-link?email=matt@lp.com", follow_redirects=False) -- assert response.location == "http://localhost/us-signin" -+ assert response.location.endswith("/us-signin") - response = client.get("us-verify-link?email=matt@lp.com", follow_redirects=True) - assert get_message("API_ERROR") in response.data - -@@ -837,10 +837,7 @@ def test_verify(app, client, get_message - us_authenticate(client) - response = client.get("us-setup", follow_redirects=False) - verify_url = response.location -- assert ( -- verify_url -- == "http://localhost/us-verify?next=http%3A%2F%2Flocalhost%2Fus-setup" -- ) -+ assert verify_url.endswith("/us-verify?next=http%3A%2F%2Flocalhost%2Fus-setup") - logout(client) - us_authenticate(client) - -@@ -1099,7 +1096,7 @@ def test_next(app, client, get_message): - data=dict(identity="matt@lp.com", passcode=requests[0]["token"]), - follow_redirects=False, - ) -- assert response.location == "http://localhost/post_login" -+ assert response.location.endswith("/post_login") - - logout(client) - response = client.post( -@@ -1109,7 +1106,7 @@ def test_next(app, client, get_message): - ), - follow_redirects=False, - ) -- assert response.location == "http://localhost/post_login" -+ assert response.location.endswith("/post_login") - - - @pytest.mark.registerable() diff --git a/no-mongodb.patch b/no-mongodb.patch index 31363e0..742eabd 100644 --- a/no-mongodb.patch +++ b/no-mongodb.patch @@ -1,22 +1,22 @@ -Index: Flask-Security-Too-4.1.2/tests/conftest.py +Index: Flask-Security-Too-5.0.2/tests/conftest.py =================================================================== ---- Flask-Security-Too-4.1.2.orig/tests/conftest.py -+++ Flask-Security-Too-4.1.2/tests/conftest.py -@@ -683,7 +683,7 @@ def client_nc(request, sqlalchemy_app): +--- Flask-Security-Too-5.0.2.orig/tests/conftest.py ++++ Flask-Security-Too-5.0.2/tests/conftest.py +@@ -862,7 +862,7 @@ def client_nc(request, sqlalchemy_app): return app.test_client(use_cookies=False) -@pytest.fixture(params=["cl-sqlalchemy", "c2", "cl-mongo", "cl-peewee"]) +@pytest.fixture(params=["cl-sqlalchemy", "c2", "cl-peewee"]) - def clients(request, app, tmpdir, realdburl): + def clients(request, app, tmpdir, realdburl, realmongodburl): if request.param == "cl-sqlalchemy": ds = sqlalchemy_setup(request, app, tmpdir, realdburl) -@@ -729,7 +729,7 @@ def get_message_local(app): +@@ -908,7 +908,7 @@ def get_message_local(app): @pytest.fixture( - params=["sqlalchemy", "sqlalchemy-session", "mongoengine", "peewee", "pony"] + params=["sqlalchemy", "sqlalchemy-session", "peewee", "pony"] ) - def datastore(request, app, tmpdir, realdburl): + def datastore(request, app, tmpdir, realdburl, realmongodburl): if request.param == "sqlalchemy": diff --git a/python-Flask-Security-Too.changes b/python-Flask-Security-Too.changes index ddac2c8..5143bc2 100644 --- a/python-Flask-Security-Too.changes +++ b/python-Flask-Security-Too.changes @@ -1,3 +1,28 @@ +------------------------------------------------------------------- +Fri Sep 30 06:52:16 UTC 2022 - Steve Kowalik + +- Upate to 5.0.2: + * Role permissions backwards compatibility bug. + * Fix Change Password regression. + * Support for WebAuthn. + * Support Two-factor recovery codes. + * Provide option to prevent user enumeration (i.e. Generic Responses). + * Support for Python 3.10. + * Support for Flask >= 2.2. + * Add custom HTML attributes to improve user experience. + * Make the required zxcvbn complexity score configurable. + * Get rid of Flask-Mail. Flask-Mailman is now the default preferred email + package. + * A delete option has been added to us-setup (form and view). + * Improve username support - the LoginForm now has a separate field for + username. + * Fix test and other failures with newer Flask-Login/Werkzeug versions. + * Fix test failures with newer Flask versions. +- Drop patch endswith-assert.patch: + * Included upstream. +- Rebase patches no-mongodb.patch and use-pyqrcodeng.patch +- Update {Build,}Requires versions. + ------------------------------------------------------------------- Thu Sep 8 06:45:05 UTC 2022 - Steve Kowalik diff --git a/python-Flask-Security-Too.spec b/python-Flask-Security-Too.spec index df8a2ff..d0d7862 100644 --- a/python-Flask-Security-Too.spec +++ b/python-Flask-Security-Too.spec @@ -19,41 +19,39 @@ %define skip_python2 1 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-Flask-Security-Too -Version: 4.1.3 +Version: 5.0.2 Release: 0 Summary: Security for Flask apps License: MIT -URL: https://github.com/jwag956/flask-security +URL: https://github.com/Flask-Middleware/flask-security Source: https://files.pythonhosted.org/packages/source/F/Flask-Security-Too/Flask-Security-Too-%{version}.tar.gz Patch0: no-mongodb.patch Patch1: use-pyqrcodeng.patch -# PATCH-FIX-UPSTREAM endswith-assert.patch gh#Flask-Middleware/flask-security#605 mcepl@suse.com -# don't test for euqality of response.headers['Location'], just for .endswith -Patch2: endswith-assert.patch -BuildRequires: %{python_module Babel >= 1.3} +BuildRequires: %{python_module Babel >= 2.9.1} BuildRequires: %{python_module Flask >= 1.1.1} -BuildRequires: %{python_module Flask-Babel} +BuildRequires: %{python_module Flask-Babel >= 2.0.0} BuildRequires: %{python_module Flask-Login >= 0.4.1} -BuildRequires: %{python_module Flask-Mail >= 0.9.1} +BuildRequires: %{python_module Flask-Mailman >= 0.3.0} BuildRequires: %{python_module Flask-Principal >= 0.4.0} -BuildRequires: %{python_module Flask-SQLAlchemy >= 2.3} +BuildRequires: %{python_module Flask-SQLAlchemy >= 2.5.1} BuildRequires: %{python_module Flask-WTF >= 0.14.3} BuildRequires: %{python_module PyQRCode >= 1.2} -BuildRequires: %{python_module SQLAlchemy >= 1.2.6} +BuildRequires: %{python_module SQLAlchemy >= 1.3.24} BuildRequires: %{python_module WTForms-lang} BuildRequires: %{python_module WTForms} BuildRequires: %{python_module Werkzeug >= 0.14.1} BuildRequires: %{python_module argon2_cffi >= 19.1.0} BuildRequires: %{python_module bcrypt >= 3.1.4} -BuildRequires: %{python_module bleach} +BuildRequires: %{python_module bleach >= 3.3.1} BuildRequires: %{python_module blinker >= 1.4} BuildRequires: %{python_module cachetools >= 3.1.0} -BuildRequires: %{python_module cryptography >= 2.1.4} +BuildRequires: %{python_module cryptography >= 3.4.8} +BuildRequires: %{python_module dateutil} BuildRequires: %{python_module email-validator >= 1.1.1} BuildRequires: %{python_module itsdangerous >= 1.1.0} BuildRequires: %{python_module passlib >= 1.7.2} BuildRequires: %{python_module peewee >= 3.7.1} -BuildRequires: %{python_module phonenumbers >= 8.11.1} +BuildRequires: %{python_module phonenumbers >= 8.12.18} BuildRequires: %{python_module pony} BuildRequires: %{python_module pytest >= 6.2.5} BuildRequires: %{python_module setuptools} @@ -61,22 +59,23 @@ BuildRequires: %{python_module zxcvbn >= 4.4.28} BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-Flask >= 1.1.1 -Requires: python-Flask-Babel +Requires: python-Flask-Babel >= 2.0.0 Requires: python-Flask-Login >= 0.4.1 Requires: python-Flask-Principal >= 0.4.0 Requires: python-Flask-WTF >= 0.14.3 Requires: python-Werkzeug >= 0.14.1 Requires: python-bcrypt >= 3.1.4 +Requires: python-bleach >= 3.3.1 Requires: python-blinker >= 1.4 -Requires: python-cryptography >= 2.1.4 +Requires: python-cryptography >= 3.4.8 Requires: python-email-validator >= 1.1.1 Requires: python-itsdangerous >= 1.1.0 Requires: python-passlib >= 1.7.2 Recommends: python-PyQRCode >= 1.2 -Recommends: python-SQLAlchemy >= 1.2.6 +Recommends: python-SQLAlchemy >= 1.3.24 Recommends: python-zxcvbn >= 4.4.28 Suggests: python-argon2_cffi >= 19.1.0 -Suggests: python-phonenumbers >= 8.11.1 +Suggests: python-phonenumbers >= 8.12.18 Conflicts: python-Flask-Security < 3.2.0 Obsoletes: python-Flask-Security < 3.2.0 Provides: python-Flask-Security = %{version} diff --git a/use-pyqrcodeng.patch b/use-pyqrcodeng.patch index 605a938..97d3678 100644 --- a/use-pyqrcodeng.patch +++ b/use-pyqrcodeng.patch @@ -1,29 +1,36 @@ -Index: Flask-Security-Too-4.1.2/flask_security/core.py +Index: Flask-Security-Too-5.0.2/flask_security/core.py =================================================================== ---- Flask-Security-Too-4.1.2.orig/flask_security/core.py -+++ Flask-Security-Too-4.1.2/flask_security/core.py -@@ -1411,7 +1411,7 @@ class Security: +--- Flask-Security-Too-5.0.2.orig/flask_security/core.py ++++ Flask-Security-Too-5.0.2/flask_security/core.py +@@ -1523,7 +1523,7 @@ class Security: and "authenticator" in cv("TWO_FACTOR_ENABLED_METHODS", app=app) ) if need_qrcode: -- self._check_modules("pyqrcode", "TWO_FACTOR or UNIFIED_SIGNIN") +- self._check_modules("qrcode", "TWO_FACTOR or UNIFIED_SIGNIN") + self._check_modules("pyqrcodeng", "TWO_FACTOR or UNIFIED_SIGNIN") need_sms = ( cv("UNIFIED_SIGNIN", app=app) -Index: Flask-Security-Too-4.1.2/flask_security/totp.py +Index: Flask-Security-Too-5.0.2/flask_security/totp.py =================================================================== ---- Flask-Security-Too-4.1.2.orig/flask_security/totp.py -+++ Flask-Security-Too-4.1.2/flask_security/totp.py -@@ -139,9 +139,9 @@ class Totp: +--- Flask-Security-Too-5.0.2.orig/flask_security/totp.py ++++ Flask-Security-Too-5.0.2/flask_security/totp.py +@@ -140,15 +140,11 @@ class Totp: .. versionadded:: 4.0.0 """ try: -- import pyqrcode +- import qrcode +- import qrcode.image.svg + import pyqrcodeng -- code = pyqrcode.create(self.get_totp_uri(username, totp)) -+ code = pyqrcodeng.create(self.get_totp_uri(username, totp)) +- image = qrcode.make( +- self.get_totp_uri(username, totp), +- image_factory=qrcode.image.svg.SvgImage, +- ) ++ image = pyqrcodeng.create(self.get_totp_uri(username, totp)) with io.BytesIO() as virtual_file: - code.svg(file=virtual_file, scale=3) +- image.save(virtual_file) ++ image.svg(virtual_file, scale=1) image_as_str = base64.b64encode(virtual_file.getvalue()).decode("ascii") + + return f"data:image/svg+xml;base64,{image_as_str}"