Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-Flask-Security-Too for 
openSUSE:Factory checked in at 2021-07-08 22:49:16
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-Flask-Security-Too (Old)
 and      /work/SRC/openSUSE:Factory/.python-Flask-Security-Too.new.2625 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-Flask-Security-Too"

Thu Jul  8 22:49:16 2021 rev:6 rq:904704 version:3.4.5

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-Flask-Security-Too/python-Flask-Security-Too.changes
      2020-07-10 14:13:08.851581897 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-Flask-Security-Too.new.2625/python-Flask-Security-Too.changes
    2021-07-08 22:49:35.347927275 +0200
@@ -1,0 +2,60 @@
+Tue Jun 15 16:37:41 UTC 2021 - Antonio Larrosa <[email protected]>
+
+- Update to 3.4.5
+  * Security Vulnerability Fix. Two CSRF vulnerabilities were
+    reported: qrcode and login. This release fixes the more severe
+    of the 2 - the /login vulnerability. The QRcode issue has a
+    much smaller risk profile since a) it is only for two-factor
+    authentication using an authenticator app b) the qrcode is only
+    available during the time the user is first setting up their
+    authentication app. The QRcode issue has been fixed in 4.0.
+  * Fixed
+    - GET on /login and /change could return the callers
+      authentication_token. This is a security concern since GETs
+      don't have CSRF protection. This bug was introduced in 3.3.0.
+  * Backwards Compatibility Concerns. Fix CSRF vulnerability on
+    /login and /change that could return the callers authentication
+    token. Now, callers can only get the authentication token on
+    successful POST calls.
+
+- Update to 3.4.4
+  * Fix 3 regressions and a couple other bugs
+  * Fixed
+    - Basic Auth broken. When the unauthenticated handler was
+      changed to provide a more uniform/consistent response - it
+      broke using Basic Auth from a browser, since it always
+      redirected rather than returning 401. Now, if the response
+      headers contain WWW-Authenticate (which is set if basic
+      @auth_required method is used), a 401 is returned. See below
+      for backwards compatibility concerns.
+    - As part of figuring out issue 359 - a redirect loop was
+      found. In release 3.3.0 code was put in to redirect to
+      :py:data:`SECURITY_POST_LOGIN_VIEW` when GET or POST was
+      called and the caller was already authenticated. The method
+      used would honor the request next query parameter. This could
+      cause redirect loops. The pre-3.3.0 behavior of redirecting
+      to :py:data:`SECURITY_POST_LOGIN_VIEW` and ignoring the next
+      parameter has been restored.
+    - Fix peewee. Turns out - due to lack of unit tests - peewee
+      hasn't worked since 'permissions' were added in 3.3.
+      Furthermore, changes in 3.4 around get_id and alternative
+      tokens also didn't work since peewee defines its own get_id
+      method.
+  * Compatibility Concerns. In 3.3.0, flask_security.auth_required
+    was changed to add a default argument if none was given. The
+    default include all current methods - session, token, and
+    basic. However basic really isn't like the others and requires
+    that we send back a WWW-Authenticate header if authentication
+    fails (and return a 401 and not redirect). basic has been
+    removed from the default set and must once again be explicitly
+    requested.
+- Rebase patch to remove another case where mongo is used:
+  * no-mongodb.patch
+- Rebase patch to fix context:
+  * fix-dependencies.patch
+- Add patch to fix failed tests (so an exception is not
+  raised if phone.data is None). Submitted upstream at
+  gh#Flask-Middleware/flask-security#495:
+  * 0001-Do-not-raise-a-TypeError-exception-if-phone.data-is-.patch
+
+-------------------------------------------------------------------

Old:
----
  Flask-Security-Too-3.4.3.tar.gz

New:
----
  0001-Do-not-raise-a-TypeError-exception-if-phone.data-is-.patch
  Flask-Security-Too-3.4.5.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-Flask-Security-Too.spec ++++++
--- /var/tmp/diff_new_pack.YlLHAO/_old  2021-07-08 22:49:35.911922923 +0200
+++ /var/tmp/diff_new_pack.YlLHAO/_new  2021-07-08 22:49:35.915922892 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-Flask-Security-Too
 #
-# Copyright (c) 2020 SUSE LLC
+# Copyright (c) 2021 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -19,7 +19,7 @@
 %define skip_python2 1
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-Flask-Security-Too
-Version:        3.4.3
+Version:        3.4.5
 Release:        0
 Summary:        Security for Flask apps
 License:        MIT
@@ -28,6 +28,7 @@
 Patch0:         no-mongodb.patch
 Patch1:         no-setup-dependencies.patch
 Patch2:         fix-dependencies.patch
+Patch3:         0001-Do-not-raise-a-TypeError-exception-if-phone.data-is-.patch
 BuildRequires:  %{python_module Babel >= 1.3}
 BuildRequires:  %{python_module Flask >= 1.0.2}
 BuildRequires:  %{python_module Flask-BabelEx >= 0.9.3}

++++++ 0001-Do-not-raise-a-TypeError-exception-if-phone.data-is-.patch ++++++
>From fc94ad58537d83b1f5500876da4a3026654645ba Mon Sep 17 00:00:00 2001
From: Antonio Larrosa <[email protected]>
Date: Tue, 15 Jun 2021 19:36:50 +0200
Subject: [PATCH] Do not raise a TypeError exception if phone.data is None

Running the tests on the openSUSE build service to generate
packages fails because a TypeError exception is raised.

```
TypeError: object of type 'NoneType' has no len()
```

This commit checks that phone.data is not None before calling
len() in the two lines where the exception is raised.
---
 flask_security/forms.py | 3 ++-
 flask_security/views.py | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/flask_security/forms.py b/flask_security/forms.py
index c793a99..83665fa 100644
--- a/flask_security/forms.py
+++ b/flask_security/forms.py
@@ -593,7 +593,8 @@ class TwoFactorSetupForm(Form, UserEmailFormMixin):
             self.setup.errors = list()
             
self.setup.errors.append(get_message("TWO_FACTOR_METHOD_NOT_AVAILABLE")[0])
             return False
-        if self.setup.data == "sms" and len(self.phone.data) > 0:
+        if (self.setup.data == "sms" and
+                self.phone.data and len(self.phone.data) > 0):
             # Somewhat bizarre - but this isn't required the first time around
             # when they select "sms". Then they get a field to fill out with
             # phone number, then Submit again.
diff --git a/flask_security/views.py b/flask_security/views.py
index c33a016..3aaca95 100644
--- a/flask_security/views.py
+++ b/flask_security/views.py
@@ -751,7 +751,8 @@ def two_factor_setup():
 
         session["tf_primary_method"] = pm
         session["tf_state"] = "validating_profile"
-        new_phone = form.phone.data if len(form.phone.data) > 0 else None
+        new_phone = form.phone.data if (form.phone.data and
+                                        len(form.phone.data) > 0) else None
         if new_phone:
             user.tf_phone_number = new_phone
             _datastore.put(user)
-- 
2.31.1

++++++ Flask-Security-Too-3.4.3.tar.gz -> Flask-Security-Too-3.4.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/CHANGES.rst 
new/Flask-Security-Too-3.4.5/CHANGES.rst
--- old/Flask-Security-Too-3.4.3/CHANGES.rst    2020-06-13 18:53:19.000000000 
+0200
+++ new/Flask-Security-Too-3.4.5/CHANGES.rst    2021-01-08 21:02:54.000000000 
+0100
@@ -14,10 +14,74 @@
 
 .. _here: https://github.com/Flask-Middleware/flask-security/issues/85
 
+Version 3.4.5
+--------------
+
+Release January x, 2021
+
+Security Vulnerability Fix.
+
+Two CSRF vulnerabilities were reported: `qrcode`_ and `login`_. This release
+fixes the more severe of the 2 - the /login vulnerability. The QRcode issue
+has a much smaller risk profile since a) it is only for two-factor 
authentication
+using an authenticator app b) the qrcode is only available during the time
+the user is first setting up their authentication app.
+The QRcode issue has been fixed in 4.0.
+
+.. _qrcode: https://github.com/Flask-Middleware/flask-security/issues/418
+.. _login: https://github.com/Flask-Middleware/flask-security/issues/421
+
+Fixed
++++++
+
+- (:issue:`421`) GET on /login and /change could return the callers 
authentication_token. This is a security
+  concern since GETs don't have CSRF protection. This bug was introduced in 
3.3.0.
+
+Backwards Compatibility Concerns
+++++++++++++++++++++++++++++++++
+
+- (:issue:`421`) Fix CSRF vulnerability on /login and /change that could 
return the callers authentication token.
+  Now, callers can only get the authentication token on successful POST calls.
+
+
+Version 3.4.4
+--------------
+
+Released July 26, 2020
+
+Bug/regression fixes.
+
+Fixed
++++++
+
+- (:issue:`359`) Basic Auth broken. When the unauthenticated handler was 
changed to provide a more
+  uniform/consistent response - it broke using Basic Auth from a browser, 
since it always redirected rather than
+  returning 401. Now, if the response headers contain  ``WWW-Authenticate``
+  (which is set if ``basic`` @auth_required method is used), a 401 is 
returned. See below
+  for backwards compatibility concerns.
+
+- (:pr:`362`) As part of figuring out issue 359 - a redirect loop was found. 
In release 3.3.0 code was put
+  in to redirect to :py:data:`SECURITY_POST_LOGIN_VIEW` when GET or POST was 
called and the caller was already authenticated. The
+  method used would honor the request ``next`` query parameter. This could 
cause redirect loops. The pre-3.3.0 behavior
+  of redirecting to :py:data:`SECURITY_POST_LOGIN_VIEW` and ignoring the 
``next`` parameter has been restored.
+
+- (:issue:`347`) Fix peewee. Turns out - due to lack of unit tests - peewee 
hasn't worked since 'permissions' were added in 3.3.
+  Furthermore, changes in 3.4 around get_id and alternative tokens also didn't 
work since peewee defines its own get_id.
+
+- (:pr:`xx`) Backport the reset_access CLI command from 4.0 - this is really 
useful for administrators.
+
+Compatibility Concerns
+++++++++++++++++++++++
+
+In 3.3.0, :func:`.auth_required` was changed to add a default argument if none 
was given. The default
+include all current methods - ``session``, ``token``, and ``basic``. However 
``basic`` really isn't like the others
+and requires that we send back a ``WWW-Authenticate`` header if authentication 
fails (and return a 401 and not redirect).
+``basic`` has been removed from the default set and must once again be 
explicitly requested.
+
 Version 3.4.3
 -------------
 
-Released June 12, 2020
+Released June 14, 2020
 
 Minor fixes for a regression and a couple other minor changes
 
@@ -25,7 +89,7 @@
 +++++
 
 - (:issue:`340`) Fix regression where tf_phone_number was required, even if 
SMS wasn't configured.
-- (:pr:`xx`) Pick up some small documentation fixes from 4.0.0.
+- (:pr:`342`) Pick up some small documentation fixes from 4.0.0.
 
 Version 3.4.2
 -------------
@@ -141,7 +205,8 @@
 this couldn't possibly work with CSRF.
 The old behavior has been restored, with the subtle change that older 
Flask-Security
 releases did not look at "next" in the form or request for the redirect,
-and now, all redirects from the login view will honor "next".
+and now, all redirects from the login view will honor "next" (N.B. see 3.4.4 - 
the
+handling of "next" has been removed due to redirect loops).
 
 Version 3.3.1
 -------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Flask-Security-Too-3.4.3/Flask_Security_Too.egg-info/PKG-INFO 
new/Flask-Security-Too-3.4.5/Flask_Security_Too.egg-info/PKG-INFO
--- old/Flask-Security-Too-3.4.3/Flask_Security_Too.egg-info/PKG-INFO   
2020-06-13 19:01:06.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/Flask_Security_Too.egg-info/PKG-INFO   
2021-01-08 21:10:44.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: Flask-Security-Too
-Version: 3.4.3
+Version: 3.4.5
 Summary: Simple security for Flask apps.
 Home-page: https://github.com/Flask-Middleware/flask-security
 Author: Matt Wright & Chris Wagner
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Flask-Security-Too-3.4.3/Flask_Security_Too.egg-info/requires.txt 
new/Flask-Security-Too-3.4.5/Flask_Security_Too.egg-info/requires.txt
--- old/Flask-Security-Too-3.4.3/Flask_Security_Too.egg-info/requires.txt       
2020-06-13 19:01:06.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/Flask_Security_Too.egg-info/requires.txt       
2021-01-08 21:10:44.000000000 +0100
@@ -12,7 +12,7 @@
 Pallets-Sphinx-Themes>=1.2.0
 Sphinx>=1.8.5
 sphinx-issues>=1.2.0
-Flask-Mongoengine>=0.9.5
+Flask-Mongoengine~=0.9.5
 peewee>=3.11.2
 Flask-SQLAlchemy>=2.3
 argon2_cffi>=19.1.0
@@ -23,8 +23,8 @@
 cryptography>=2.3.1
 isort>=4.2.2
 mock>=1.3.0
-mongoengine>=0.15.3
-mongomock>=3.14.0
+mongoengine~=0.19.1
+mongomock~=3.19.0
 msgcheck>=2.9
 pony>=0.7.11
 phonenumberslite>=8.11.1
@@ -32,6 +32,7 @@
 pydocstyle>=1.0.0
 pymysql>=0.9.3
 pyqrcode>=1.2
+pytest==4.6.11
 pytest-black>=0.3.8
 pytest-cache>=1.0
 pytest-cov>=2.5.1
@@ -45,7 +46,7 @@
 Pallets-Sphinx-Themes>=1.2.0
 Sphinx>=1.8.5
 sphinx-issues>=1.2.0
-Flask-Mongoengine>=0.9.5
+Flask-Mongoengine~=0.9.5
 peewee>=3.11.2
 Flask-SQLAlchemy>=2.3
 argon2_cffi>=19.1.0
@@ -56,8 +57,8 @@
 cryptography>=2.3.1
 isort>=4.2.2
 mock>=1.3.0
-mongoengine>=0.15.3
-mongomock>=3.14.0
+mongoengine~=0.19.1
+mongomock~=3.19.0
 msgcheck>=2.9
 pony>=0.7.11
 phonenumberslite>=8.11.1
@@ -65,6 +66,7 @@
 pydocstyle>=1.0.0
 pymysql>=0.9.3
 pyqrcode>=1.2
+pytest==4.6.11
 pytest-black>=0.3.8
 pytest-cache>=1.0
 pytest-cov>=2.5.1
@@ -82,7 +84,7 @@
 sphinx-issues>=1.2.0
 
 [tests]
-Flask-Mongoengine>=0.9.5
+Flask-Mongoengine~=0.9.5
 peewee>=3.11.2
 Flask-SQLAlchemy>=2.3
 argon2_cffi>=19.1.0
@@ -93,8 +95,8 @@
 cryptography>=2.3.1
 isort>=4.2.2
 mock>=1.3.0
-mongoengine>=0.15.3
-mongomock>=3.14.0
+mongoengine~=0.19.1
+mongomock~=3.19.0
 msgcheck>=2.9
 pony>=0.7.11
 phonenumberslite>=8.11.1
@@ -102,6 +104,7 @@
 pydocstyle>=1.0.0
 pymysql>=0.9.3
 pyqrcode>=1.2
+pytest==4.6.11
 pytest-black>=0.3.8
 pytest-cache>=1.0
 pytest-cov>=2.5.1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/PKG-INFO 
new/Flask-Security-Too-3.4.5/PKG-INFO
--- old/Flask-Security-Too-3.4.3/PKG-INFO       2020-06-13 19:01:06.000000000 
+0200
+++ new/Flask-Security-Too-3.4.5/PKG-INFO       2021-01-08 21:10:44.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: Flask-Security-Too
-Version: 3.4.3
+Version: 3.4.5
 Summary: Simple security for Flask apps.
 Home-page: https://github.com/Flask-Middleware/flask-security
 Author: Matt Wright & Chris Wagner
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/docs/conf.py 
new/Flask-Security-Too-3.4.5/docs/conf.py
--- old/Flask-Security-Too-3.4.3/docs/conf.py   2020-06-13 18:53:19.000000000 
+0200
+++ new/Flask-Security-Too-3.4.5/docs/conf.py   2021-01-08 21:02:54.000000000 
+0100
@@ -58,7 +58,7 @@
 # built documents.
 #
 # The short X.Y version.
-version = "3.4.3"
+version = "3.4.5"
 # The full version, including alpha/beta/rc tags.
 release = version
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/docs/customizing.rst 
new/Flask-Security-Too-3.4.5/docs/customizing.rst
--- old/Flask-Security-Too-3.4.3/docs/customizing.rst   2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/docs/customizing.rst   2021-01-08 
21:02:54.000000000 +0100
@@ -196,7 +196,7 @@
 
 
     from flask_mail import Message
-    
+
     # Setup the task
     @celery.task
     def send_flask_mail(**kwargs):
@@ -326,19 +326,23 @@
 401, 403, Oh My
 +++++++++++++++
 For a very long read and discussion; look at `this`_. Out of the box, 
Flask-Security in
-tandem with Flask-Login, behaves as follows:
+tandem with Flask-Login, behave as follows:
 
-    * If authentication fails as the result of a `@login_required`, 
`@auth_required`,
-      `@http_auth_required`, or `@token_auth_required` then if the request 
'wants' a JSON
+    * If authentication fails as the result of a `@login_required`, 
`@auth_required("session", "token")`,
+      or `@token_auth_required` then if the request 'wants' a JSON
       response, :meth:`.Security.render_json` is called with a 401 status 
code. If not
       then flask_login.LoginManager.unauthorized() is called. By default THAT 
will redirect to
       a login view.
 
+    * If authentication fails as the result of a `@http_auth_required` or 
`@auth_required("basic")`
+      then a 401 is returned along with the http header ``WWW-Authenticate`` 
set to
+      ``Basic realm="xxxx"``. The realm name is defined by 
:py:data:`SECURITY_DEFAULT_HTTP_AUTH_REALM`.
+
     * If authorization fails as the result of `@roles_required`, 
`@roles_accepted`,
       `@permissions_required`, or `@permissions_accepted`, then if the request 
'wants' a JSON
       response, :meth:`.Security.render_json` is called with a 403 status 
code. If not,
-      then if *SECURITY_UNAUTHORIZED_VIEW* is defined, the response will 
redirected.
-      If *SECURITY_UNAUTHORIZED_VIEW* is not defined, then ``abort(403)`` is 
called.
+      then if :py:data:`SECURITY_UNAUTHORIZED_VIEW` is defined, the response 
will redirected.
+      If :py:data:`SECURITY_UNAUTHORIZED_VIEW` is not defined, then 
``abort(403)`` is called.
 
 All this can be easily changed by registering any or all of 
:meth:`.Security.render_json`,
 :meth:`.Security.unauthn_handler` and :meth:`.Security.unauthz_handler`.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/docs/quickstart.rst 
new/Flask-Security-Too-3.4.5/docs/quickstart.rst
--- old/Flask-Security-Too-3.4.3/docs/quickstart.rst    2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/docs/quickstart.rst    2021-01-08 
21:02:54.000000000 +0100
@@ -273,6 +273,7 @@
     class Role(db.Document, RoleMixin):
         name = db.StringField(max_length=80, unique=True)
         description = db.StringField(max_length=255)
+        permissions = db.StringField(max_length=255)
 
     class User(db.Document, UserMixin):
         email = db.StringField(max_length=255)
@@ -348,15 +349,18 @@
     # Create database connection object
     db = FlaskDB(app)
 
-    class Role(db.Model, RoleMixin):
+    class Role(RoleMixin, db.Model):
         name = CharField(unique=True)
         description = TextField(null=True)
+        permissions = TextField(null=True)
 
-    class User(db.Model, UserMixin):
+    # N.B. order is important since db.Model also contains a get_id() -
+    # we need the one from UserMixin.
+    class User(UserMixin, db.Model):
         email = TextField()
         password = TextField()
         active = BooleanField(default=True)
-        fs_uniquifier = TextField()
+        fs_uniquifier = TextField(unique=True, null=False)
         confirmed_at = DateTimeField(null=True)
 
     class UserRoles(db.Model):
@@ -368,6 +372,9 @@
         name = property(lambda self: self.role.name)
         description = property(lambda self: self.role.description)
 
+        def get_permissions(self):
+            return self.role.get_permissions()
+
     # Setup Flask-Security
     user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles)
     security = Security(app, user_datastore)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/flask_security/__init__.py 
new/Flask-Security-Too-3.4.5/flask_security/__init__.py
--- old/Flask-Security-Too-3.4.3/flask_security/__init__.py     2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/flask_security/__init__.py     2021-01-08 
21:02:54.000000000 +0100
@@ -101,4 +101,4 @@
     verify_and_update_password,
 )
 
-__version__ = "3.4.3"
+__version__ = "3.4.5"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/flask_security/cli.py 
new/Flask-Security-Too-3.4.5/flask_security/cli.py
--- old/Flask-Security-Too-3.4.3/flask_security/cli.py  2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/flask_security/cli.py  2021-01-08 
21:02:54.000000000 +0100
@@ -121,7 +121,7 @@
 @with_appcontext
 @commit
 def roles_add(user, role):
-    """Add user to role."""
+    """Add role to user."""
     user, role = _datastore._prepare_role_modify_args(user, role)
     if user is None:
         raise click.UsageError("Cannot find user.")
@@ -129,7 +129,7 @@
         raise click.UsageError("Cannot find role.")
     if _datastore.add_role_to_user(user, role):
         click.secho(
-            'Role "{0}" added to user "{1}" ' "successfully.".format(role, 
user),
+            'Role "{0}" added to user "{1}" ' 
"successfully.".format(role.name, user),
             fg="green",
         )
     else:
@@ -142,7 +142,7 @@
 @with_appcontext
 @commit
 def roles_remove(user, role):
-    """Remove user from role."""
+    """Remove role from user."""
     user, role = _datastore._prepare_role_modify_args(user, role)
     if user is None:
         raise click.UsageError("Cannot find user.")
@@ -150,7 +150,8 @@
         raise click.UsageError("Cannot find role.")
     if _datastore.remove_role_from_user(user, role):
         click.secho(
-            'Role "{0}" removed from user "{1}" ' "successfully.".format(role, 
user),
+            'Role "{0}" removed from user "{1}" '
+            "successfully.".format(role.name, user),
             fg="green",
         )
     else:
@@ -165,7 +166,7 @@
     """Activate a user."""
     user_obj = _datastore.get_user(user)
     if user_obj is None:
-        raise click.UsageError("ERROR: User not found.")
+        raise click.UsageError("User not found.")
     if _datastore.activate_user(user_obj):
         click.secho('User "{0}" has been activated.'.format(user), fg="green")
     else:
@@ -180,8 +181,29 @@
     """Deactivate a user."""
     user_obj = _datastore.get_user(user)
     if user_obj is None:
-        raise click.UsageError("ERROR: User not found.")
+        raise click.UsageError("User not found.")
     if _datastore.deactivate_user(user_obj):
         click.secho('User "{0}" has been deactivated.'.format(user), 
fg="green")
     else:
         click.secho('User "{0}" was already deactivated.'.format(user), 
fg="yellow")
+
+
[email protected](
+    "reset_access",
+    help="Reset all authentication credentials for user."
+    " This includes session, auth token, two-factor"
+    " and unified sign in secrets. ",
+)
[email protected]("user")
+@with_appcontext
+@commit
+def users_reset_access(user):
+    """ Reset all authentication tokens etc."""
+    user_obj = _datastore.get_user(user)
+    if user_obj is None:
+        raise click.UsageError("User not found.")
+    _datastore.reset_user_access(user_obj)
+    click.secho(
+        'User "{user}" authentication credentials have been 
reset.'.format(user=user),
+        fg="green",
+    )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/flask_security/core.py 
new/Flask-Security-Too-3.4.5/flask_security/core.py
--- old/Flask-Security-Too-3.4.3/flask_security/core.py 2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/flask_security/core.py 2021-01-08 
21:02:54.000000000 +0100
@@ -678,6 +678,9 @@
         Caller must commit to DB.
 
         .. versionadded:: 3.3.0
+
+        .. deprecated:: 3.4.4
+            Use :meth:`.UserDatastore.remove_permissions_from_role`
         """
         if hasattr(self, "permissions"):
             current_perms = self.get_permissions()
@@ -688,7 +691,7 @@
             else:
                 perms = {permissions}
             self.permissions = ",".join(current_perms.union(perms))
-        else:
+        else:  # pragma: no cover
             raise NotImplementedError("Role model doesn't have permissions")
 
     def remove_permissions(self, permissions):
@@ -700,6 +703,9 @@
         Caller must commit to DB.
 
         .. versionadded:: 3.3.0
+
+        .. deprecated:: 3.4.4
+            Use :meth:`.UserDatastore.remove_permissions_from_role`
         """
         if hasattr(self, "permissions"):
             current_perms = self.get_permissions()
@@ -710,7 +716,7 @@
             else:
                 perms = {permissions}
             self.permissions = ",".join(current_perms.difference(perms))
-        else:
+        else:  # pragma: no cover
             raise NotImplementedError("Role model doesn't have permissions")
 
 
@@ -791,9 +797,8 @@
 
         """
         for role in self.roles:
-            if hasattr(role, "permissions"):
-                if permission in role.get_permissions():
-                    return True
+            if permission in role.get_permissions():
+                return True
         return False
 
     def get_security_payload(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/flask_security/datastore.py 
new/Flask-Security-Too-3.4.5/flask_security/datastore.py
--- old/Flask-Security-Too-3.4.3/flask_security/datastore.py    2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/flask_security/datastore.py    2021-01-08 
21:02:54.000000000 +0100
@@ -202,6 +202,48 @@
             self.put(user)
         return rv
 
+    def add_permissions_to_role(self, role, permissions):
+        """Add one or more permissions to role.
+
+        :param role: The role to modify. Can be a Role object or
+            string role name
+        :param permissions: a set, list, or single string.
+        :return: True if permissions added, False if role doesn't exist.
+
+        Caller must commit to DB.
+
+        .. versionadded:: 3.4.4
+        """
+
+        rv = False
+        user, role = self._prepare_role_modify_args(None, role)
+        if role:
+            rv = True
+            role.add_permissions(permissions)
+            self.put(role)
+        return rv
+
+    def remove_permissions_from_role(self, role, permissions):
+        """Remove one or more permissions from a role.
+
+        :param role: The role to modify. Can be a Role object or
+            string role name
+        :param permissions: a set, list, or single string.
+        :return: True if permissions removed, False if role doesn't exist.
+
+        Caller must commit to DB.
+
+        .. versionadded:: 3.4.4
+        """
+
+        rv = False
+        user, role = self._prepare_role_modify_args(None, role)
+        if role:
+            rv = True
+            role.remove_permissions(permissions)
+            self.put(role)
+        return rv
+
     def toggle_active(self, user):
         """Toggles a user's active status. Always returns True."""
         user.active = not user.active
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Flask-Security-Too-3.4.3/flask_security/decorators.py 
new/Flask-Security-Too-3.4.5/flask_security/decorators.py
--- old/Flask-Security-Too-3.4.3/flask_security/decorators.py   2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/flask_security/decorators.py   2021-01-08 
21:02:54.000000000 +0100
@@ -63,21 +63,29 @@
 def default_unauthn_handler(mechanisms, headers=None):
     """ Default callback for failures to authenticate
 
-    If caller wants JSON - return 401
+    If caller wants JSON - return 401.
+    If caller wants BasicAuth - return 401 (the WWW-Authenticate header is 
set).
     Otherwise - assume caller is html and redirect if possible to a login view.
     We let Flask-Login handle this.
 
     """
+    headers = headers or {}
     msg = get_message("UNAUTHENTICATED")[0]
 
     if config_value("BACKWARDS_COMPAT_UNAUTHN"):
         return _get_unauthenticated_response(headers=headers)
     if _security._want_json(request):
-        # Ignore headers since today, the only thing in there might be 
WWW-Authenticate
-        # and we never want to send that in a JSON response (browsers will 
intercept
-        # that and pop up their own login form).
+        # Remove WWW-Authenticate from headers for JSON responses since
+        # browsers will intercept that and pop up their own login form.
+        headers.pop("WWW-Authenticate", None)
         payload = json_error_response(errors=msg)
-        return _security._render_json(payload, 401, None, None)
+        return _security._render_json(payload, 401, headers, None)
+
+    # Basic-Auth is often used to provide a browser based login form and then 
the
+    # browser will always add the BasicAuth credentials. For that to work we 
need to
+    # return 401 and not redirect to our login view.
+    if "WWW-Authenticate" in headers:
+        return Response(msg, 401, headers)
     return _security.login_manager.unauthorized()
 
 
@@ -212,6 +220,9 @@
 
     :param realm: optional realm name
 
+    If authentication fails, then a 401 with the 'WWW-Authenticate' header set 
will be
+    returned.
+
     Once authenticated, if so configured, CSRF protection will be tested.
     """
 
@@ -269,7 +280,7 @@
             return 'Dashboard'
 
     :param auth_methods: Specified mechanisms (token, basic, session). If not 
specified
-        then all current available mechanisms will be tried.
+        then all current available mechanisms (except "basic") will be tried.
     :kwparam within: Add 'freshness' check to authentication. Is either an int
         specifying # of minutes, or a callable that returns a timedelta. For 
timedeltas,
         timedelta.total_seconds() is used for the calculations:
@@ -297,6 +308,11 @@
     On authentication failure `.Security.unauthorized_callback` (deprecated)
     or :meth:`.Security.unauthn_handler` will be called.
 
+    .. note::
+        If "basic" is specified in addition to other methods, then if 
authentication
+        fails, a 401 with the "WWW-Authenticate" header will be returned - 
rather than
+        being redirected to the login view.
+
     .. versionchanged:: 3.3.0
        If ``auth_methods`` isn't specified, then all will be tried. 
Authentication
        mechanisms will always be tried in order of ``token``, ``session``, 
``basic``
@@ -305,6 +321,9 @@
     .. versionchanged:: 3.4.0
         Added ``within`` and ``grace`` parameters to enforce a freshness check.
 
+    .. versionchanged:: 3.4.4
+        If ``auth_methods`` isn't specified try all mechanisms EXCEPT 
``basic``.
+
     """
 
     login_mechanisms = {
@@ -314,7 +333,7 @@
     }
     mechanisms_order = ["token", "session", "basic"]
     if not auth_methods:
-        auth_methods = {"basic", "session", "token"}
+        auth_methods = {"session", "token"}
     else:
         auth_methods = [am for am in auth_methods]
 
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/da_DK/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/da_DK/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/de_DE/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/de_DE/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/es_ES/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/es_ES/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.mo
 differ
Binary files 
old/Flask-Security-Too-3.4.3/flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.mo
 and 
new/Flask-Security-Too-3.4.5/flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.mo
 differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/flask_security/views.py 
new/Flask-Security-Too-3.4.5/flask_security/views.py
--- old/Flask-Security-Too-3.4.3/flask_security/views.py        2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/flask_security/views.py        2021-01-08 
21:02:54.000000000 +0100
@@ -142,21 +142,20 @@
     """
 
     if current_user.is_authenticated and request.method == "POST":
-        # Just redirect current_user to POST_LOGIN_VIEW (or next).
+        # Just redirect current_user to POST_LOGIN_VIEW.
         # While its tempting to try to logout the current user and login the
         # new requested user - that simply doesn't work with CSRF.
 
-        # While this is close to anonymous_user_required - it differs in that
-        # it uses get_post_login_redirect which correctly handles 'next'.
-        # TODO: consider changing anonymous_user_required to also call
-        # get_post_login_redirect - not sure why it never has?
+        # This does NOT use get_post_login_redirect() so that it doesn't look 
at
+        # 'next' - which can cause infinite redirect loops
+        # (see test_common::test_authenticated_loop)
         if _security._want_json(request):
             payload = json_error_response(
                 errors=get_message("ANONYMOUS_USER_REQUIRED")[0]
             )
             return _security._render_json(payload, 400, None, None)
         else:
-            return redirect(get_post_login_redirect())
+            return redirect(get_url(_security.post_login_view))
 
     form_class = _security.login_form
 
@@ -182,18 +181,17 @@
         login_user(form.user, remember=remember_me, authn_via=["password"])
         after_this_request(_commit)
 
-        if not _security._want_json(request):
-            return redirect(get_post_login_redirect())
+        if _security._want_json(request):
+            return base_render_json(form, include_auth_token=True)
+        return redirect(get_post_login_redirect())
 
     if _security._want_json(request):
         if current_user.is_authenticated:
             form.user = current_user
-        return base_render_json(form, include_auth_token=True)
+        return base_render_json(form)
 
     if current_user.is_authenticated:
-        # Basically a no-op if authenticated - just perform the same
-        # post-login redirect as if user just logged in.
-        return redirect(get_post_login_redirect())
+        return redirect(get_url(_security.post_login_view))
     else:
         return _security.render_template(
             config_value("LOGIN_USER_TEMPLATE"), login_user_form=form, 
**_ctx("login")
@@ -625,16 +623,18 @@
     if form.validate_on_submit():
         after_this_request(_commit)
         change_user_password(current_user._get_current_object(), 
form.new_password.data)
-        if not _security._want_json(request):
-            do_flash(*get_message("PASSWORD_CHANGE"))
-            return redirect(
-                get_url(_security.post_change_view)
-                or get_url(_security.post_login_view)
-            )
+        if _security._want_json(request):
+            form.user = current_user
+            return base_render_json(form, include_auth_token=True)
+
+        do_flash(*get_message("PASSWORD_CHANGE"))
+        return redirect(
+            get_url(_security.post_change_view) or 
get_url(_security.post_login_view)
+        )
 
     if _security._want_json(request):
         form.user = current_user
-        return base_render_json(form, include_auth_token=True)
+        return base_render_json(form)
 
     return _security.render_template(
         config_value("CHANGE_PASSWORD_TEMPLATE"),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/pytest.ini 
new/Flask-Security-Too-3.4.5/pytest.ini
--- old/Flask-Security-Too-3.4.3/pytest.ini     2020-06-13 18:53:19.000000000 
+0200
+++ new/Flask-Security-Too-3.4.5/pytest.ini     2021-01-08 21:02:54.000000000 
+0100
@@ -1,5 +1,5 @@
 [pytest]
-addopts = -rs --cov flask_security --cov-report term-missing --black --flake8 
--cache-clear
+addopts = -rs --cov flask_security --cov-report term-missing --flake8 
--cache-clear
 flake8-max-line-length = 88
 flake8-ignore =
     tests/view_scaffold.py E402
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/setup.py 
new/Flask-Security-Too-3.4.5/setup.py
--- old/Flask-Security-Too-3.4.3/setup.py       2020-06-13 18:53:19.000000000 
+0200
+++ new/Flask-Security-Too-3.4.5/setup.py       2021-01-08 21:02:54.000000000 
+0100
@@ -13,7 +13,7 @@
     version = re.search(r'__version__ = "(.*?)"', f.read()).group(1)
 
 tests_require = [
-    "Flask-Mongoengine>=0.9.5",
+    "Flask-Mongoengine~=0.9.5",
     "peewee>=3.11.2",
     "Flask-SQLAlchemy>=2.3",
     "argon2_cffi>=19.1.0",
@@ -24,8 +24,8 @@
     "cryptography>=2.3.1",
     "isort>=4.2.2",
     "mock>=1.3.0",
-    "mongoengine>=0.15.3",
-    "mongomock>=3.14.0",
+    "mongoengine~=0.19.1",
+    "mongomock~=3.19.0",
     "msgcheck>=2.9",
     "pony>=0.7.11",
     "phonenumberslite>=8.11.1",
@@ -33,6 +33,7 @@
     "pydocstyle>=1.0.0",
     "pymysql>=0.9.3",
     "pyqrcode>=1.2",
+    "pytest==4.6.11",
     "pytest-black>=0.3.8",
     "pytest-cache>=1.0",
     "pytest-cov>=2.5.1",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/tests/conftest.py 
new/Flask-Security-Too-3.4.5/tests/conftest.py
--- old/Flask-Security-Too-3.4.3/tests/conftest.py      2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/tests/conftest.py      2021-01-08 
21:02:54.000000000 +0100
@@ -235,6 +235,7 @@
     class Role(db.Document, RoleMixin):
         name = db.StringField(required=True, unique=True, max_length=80)
         description = db.StringField(max_length=255)
+        permissions = db.StringField(max_length=255)
         meta = {"db_alias": db_name}
 
     class User(db.Document, UserMixin):
@@ -347,6 +348,7 @@
         id = Column(Integer(), primary_key=True)
         name = Column(String(80), unique=True)
         description = Column(String(255))
+        permissions = Column(String(255))
 
     class User(Base, UserMixin):
         __tablename__ = "user"
@@ -424,12 +426,14 @@
 
     db = FlaskDB(app)
 
-    class Role(db.Model, RoleMixin):
+    class Role(RoleMixin, db.Model):
         name = CharField(unique=True, max_length=80)
         description = TextField(null=True)
+        permissions = TextField(null=True)
 
-    class User(db.Model, UserMixin):
+    class User(UserMixin, db.Model):
         email = TextField()
+        fs_uniquifier = TextField(unique=True, null=False)
         username = TextField()
         security_number = IntegerField(null=True)
         password = TextField(null=True)
@@ -455,6 +459,9 @@
         name = property(lambda self: self.role.name)
         description = property(lambda self: self.role.description)
 
+        def get_permissions(self):
+            return self.role.get_permissions()
+
     with app.app_context():
         for Model in (Role, User, UserRoles):
             Model.drop_table()
@@ -600,6 +607,27 @@
     return app.test_client(use_cookies=False)
 
 
[email protected](params=["cl-sqlalchemy", "c2", "cl-mongo", "cl-peewee"])
+def clients(request, app, tmpdir, realdburl):
+    if request.param == "cl-sqlalchemy":
+        ds = sqlalchemy_setup(request, app, tmpdir, realdburl)
+    elif request.param == "c2":
+        ds = sqlalchemy_session_setup(request, app, tmpdir, realdburl)
+    elif request.param == "cl-mongo":
+        ds = mongoengine_setup(request, app, tmpdir, realdburl)
+    elif request.param == "cl-peewee":
+        ds = peewee_setup(request, app, tmpdir, realdburl)
+    elif request.param == "cl-pony":
+        # Not working yet.
+        ds = pony_setup(request, app, tmpdir, realdburl)
+    app.security = Security(app, datastore=ds)
+    populate_data(app)
+    if request.param == "cl-peewee":
+        # peewee is insistent on a single connection?
+        ds.db.close_db(None)
+    return app.test_client()
+
+
 @pytest.yield_fixture()
 def in_app_context(request, sqlalchemy_app):
     app = sqlalchemy_app()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/tests/test_common.py 
new/Flask-Security-Too-3.4.5/tests/test_common.py
--- old/Flask-Security-Too-3.4.3/tests/test_common.py   2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/tests/test_common.py   2021-01-08 
21:02:54.000000000 +0100
@@ -98,9 +98,9 @@
 def test_get_already_authenticated_next(client):
     response = authenticate(client, follow_redirects=True)
     assert b"Welcome [email protected]" in response.data
-    # This should override post_login_view
+    # This should NOT override post_login_view due to potential redirect loops.
     response = client.get("/login?next=/page1", follow_redirects=True)
-    assert b"Page 1" in response.data
+    assert b"Post Login" in response.data
 
 
 @pytest.mark.settings(post_login_view="/post_login")
@@ -110,8 +110,9 @@
     data = dict(email="[email protected]", password="password")
     response = client.post("/login", data=data, follow_redirects=True)
     assert b"Post Login" in response.data
+    # This should NOT override post_login_view due to potential redirect loops.
     response = client.post("/login?next=/page1", data=data, 
follow_redirects=True)
-    assert b"Page 1" in response.data
+    assert b"Post Login" in response.data
 
 
 def test_login_form(client):
@@ -268,42 +269,42 @@
 
 
 @pytest.mark.settings(unauthorized_view="/unauthz")
-def test_roles_accepted(client):
+def test_roles_accepted(clients):
     # This specificaly tests that we can pass a URL for unauthorized_view.
     for user in ("[email protected]", "[email protected]"):
-        authenticate(client, user)
-        response = client.get("/admin_or_editor")
+        authenticate(clients, user)
+        response = clients.get("/admin_or_editor")
         assert b"Admin or Editor Page" in response.data
-        logout(client)
+        logout(clients)
 
-    authenticate(client, "[email protected]")
-    response = client.get("/admin_or_editor", follow_redirects=True)
+    authenticate(clients, "[email protected]")
+    response = clients.get("/admin_or_editor", follow_redirects=True)
     assert b"Unauthorized" in response.data
 
 
 @pytest.mark.settings(unauthorized_view="unauthz")
-def test_permissions_accepted(client):
+def test_permissions_accepted(clients):
     for user in ("[email protected]", "[email protected]"):
-        authenticate(client, user)
-        response = client.get("/admin_perm")
+        authenticate(clients, user)
+        response = clients.get("/admin_perm")
         assert b"Admin Page with full-write or super" in response.data
-        logout(client)
+        logout(clients)
 
-    authenticate(client, "[email protected]")
-    response = client.get("/admin_perm", follow_redirects=True)
+    authenticate(clients, "[email protected]")
+    response = clients.get("/admin_perm", follow_redirects=True)
     assert b"Unauthorized" in response.data
 
 
 @pytest.mark.settings(unauthorized_view="unauthz")
-def test_permissions_required(client):
+def test_permissions_required(clients):
     for user in ["[email protected]"]:
-        authenticate(client, user)
-        response = client.get("/admin_perm_required")
+        authenticate(clients, user)
+        response = clients.get("/admin_perm_required")
         assert b"Admin Page required" in response.data
-        logout(client)
+        logout(clients)
 
-    authenticate(client, "[email protected]")
-    response = client.get("/admin_perm_required", follow_redirects=True)
+    authenticate(clients, "[email protected]")
+    response = clients.get("/admin_perm_required", follow_redirects=True)
     assert b"Unauthorized" in response.data
 
 
@@ -314,15 +315,15 @@
 
 
 @pytest.mark.settings(unauthorized_view="unauthz")
-def test_multiple_role_required(client):
+def test_multiple_role_required(clients):
     for user in ("[email protected]", "[email protected]"):
-        authenticate(client, user)
-        response = client.get("/admin_and_editor", follow_redirects=True)
+        authenticate(clients, user)
+        response = clients.get("/admin_and_editor", follow_redirects=True)
         assert b"Unauthorized" in response.data
-        client.get("/logout")
+        clients.get("/logout")
 
-    authenticate(client, "[email protected]")
-    response = client.get("/admin_and_editor", follow_redirects=True)
+    authenticate(clients, "[email protected]")
+    response = clients.get("/admin_and_editor", follow_redirects=True)
     assert b"Admin and Editor Page" in response.data
 
 
@@ -365,6 +366,15 @@
 
 
 def test_http_auth(client):
+    # browsers expect 401 response with WWW-Authenticate header - which will 
prompt
+    # them to pop up a login form.
+    response = client.get("/http", headers={})
+    assert response.status_code == 401
+    assert b"You are not authenticated" in response.data
+    assert "WWW-Authenticate" in response.headers
+    assert 'Basic realm="Login Required"' == 
response.headers["WWW-Authenticate"]
+
+    # Now provide correct credentials
     response = client.get(
         "/http",
         headers={
@@ -505,8 +515,10 @@
     assert b"Basic" in response.data
 
     response = client.get("/multi_auth")
-    # Default unauthn is to redirect
-    assert response.status_code == 302
+    # Default unauthn with basic is to return 401 with WWW-Authenticate Header
+    # so that browser pops up a username/password dialog
+    assert response.status_code == 401
+    assert "WWW-Authenticate" in response.headers
 
 
 @pytest.mark.settings(backwards_compat_unauthn=True)
@@ -523,7 +535,6 @@
     assert 'Basic realm="Login Required"' == 
response.headers["WWW-Authenticate"]
 
     response = client.get("/multi_auth")
-    print(response.headers)
     assert response.status_code == 401
 
 
@@ -540,6 +551,20 @@
     assert b"Session" in response.data
 
 
+def test_authenticated_loop(client):
+    # If user is already authenticated say via session, and then hits an 
endpoint
+    # protected with @auth_token_required() - then they will be redirected to 
the login
+    # page which will simply note the current user is already logged in and 
redirect
+    # to POST_LOGIN_VIEW. Between 3.3.0 and 3.4.4 - this redirect would honor 
the 'next'
+    # parameter - thus redirecting back to the endpoint that caused the 
redirect in the
+    # first place - thus an infinite loop.
+    authenticate(client)
+
+    response = client.get("/token", follow_redirects=True)
+    assert response.status_code == 200
+    assert b"Home Page" in response.data
+
+
 def test_user_deleted_during_session_reverts_to_anonymous_user(app, client):
     authenticate(client)
 
@@ -724,3 +749,26 @@
     assert response.status_code == 200
     end_nqueries = get_num_queries(app.security.datastore)
     assert current_nqueries is None or end_nqueries == (current_nqueries + 2)
+
+
[email protected]()
+def test_no_get_auth_token(app, client):
+    # Test that GETs don't return an auth token. This is a security issue since
+    # GETs aren't protected with CSRF
+    authenticate(client)
+    response = client.get(
+        "/login?include_auth_token", headers={"Content-Type": 
"application/json"}
+    )
+    assert "authentication_token" not in response.json["response"]["user"]
+
+    data = dict(
+        password="password",
+        new_password="new strong password",
+        new_password_confirm="new strong password",
+    )
+    response = client.get(
+        "/change?include_auth_token",
+        json=data,
+        headers={"Content-Type": "application/json"},
+    )
+    assert "authentication_token" not in response.json["response"]["user"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/tests/test_configuration.py 
new/Flask-Security-Too-3.4.5/tests/test_configuration.py
--- old/Flask-Security-Too-3.4.3/tests/test_configuration.py    2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/tests/test_configuration.py    2021-01-08 
21:02:54.000000000 +0100
@@ -35,8 +35,10 @@
         "/http",
         headers={"Authorization": "Basic %s" % 
base64.b64encode(b"[email protected]:bogus")},
     )
-    assert response.status_code == 302
-    assert response.headers["Location"] == 
"http://localhost/custom_login?next=%2Fhttp";
+    assert response.status_code == 401
+    assert b"You are not authenticated" in response.data
+    assert "WWW-Authenticate" in response.headers
+    assert 'Basic realm="Custom Realm"' == response.headers["WWW-Authenticate"]
 
 
 @pytest.mark.settings(login_user_template="custom_security/login_user.html")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/tests/test_datastore.py 
new/Flask-Security-Too-3.4.5/tests/test_datastore.py
--- old/Flask-Security-Too-3.4.3/tests/test_datastore.py        2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/tests/test_datastore.py        2021-01-08 
21:02:54.000000000 +0100
@@ -258,6 +258,7 @@
         user = datastore.find_user(email="[email protected]")
         assert user.has_role("test1") is True
         assert user.has_permission("read") is True
+        assert user.has_permission("write") is False
 
 
 def test_permissions_strings(app, datastore):
@@ -305,20 +306,22 @@
 
         t1 = ds.find_role("test1")
         assert perms == t1.get_permissions()
-        orig_update_time = t1.update_datetime
-        assert t1.update_datetime <= datetime.datetime.utcnow()
+        if hasattr(t1, "update_datetime"):
+            orig_update_time = t1.update_datetime
+            assert t1.update_datetime <= datetime.datetime.utcnow()
 
-        t1.add_permissions("execute")
+        ds.add_permissions_to_role(t1, "execute")
         ds.commit()
 
         t1 = ds.find_role("test1")
         assert perms.union({"execute"}) == t1.get_permissions()
 
-        t1.remove_permissions("read")
+        ds.remove_permissions_from_role(t1, "read")
         ds.commit()
         t1 = ds.find_role("test1")
         assert {"write", "execute"} == t1.get_permissions()
-        assert t1.update_datetime > orig_update_time
+        if hasattr(t1, "update_datetime"):
+            assert t1.update_datetime > orig_update_time
 
 
 def test_get_permissions(app, datastore):
@@ -350,13 +353,13 @@
         assert {"read", "write"} == t1.get_permissions()
 
         # send in a list
-        t1.add_permissions(["execute", "whatever"])
+        ds.add_permissions_to_role(t1, ["execute", "whatever"])
         ds.commit()
 
         t1 = ds.find_role("test1")
         assert {"read", "write", "execute", "whatever"} == t1.get_permissions()
 
-        t1.remove_permissions(["read", "whatever"])
+        ds.remove_permissions_from_role(t1, ["read", "whatever"])
         ds.commit()
         assert {"write", "execute"} == t1.get_permissions()
 
@@ -366,13 +369,13 @@
         ds.commit()
 
         t2 = ds.find_role("test2")
-        t2.add_permissions({"execute", "whatever"})
+        ds.add_permissions_to_role(t2, {"execute", "whatever"})
         ds.commit()
 
         t2 = ds.find_role("test2")
         assert {"read", "write", "execute", "whatever"} == t2.get_permissions()
 
-        t2.remove_permissions({"read", "whatever"})
+        ds.remove_permissions_from_role(t2, {"read", "whatever"})
         ds.commit()
         assert {"write", "execute"} == t2.get_permissions()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/tests/test_response.py 
new/Flask-Security-Too-3.4.5/tests/test_response.py
--- old/Flask-Security-Too-3.4.3/tests/test_response.py 2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/tests/test_response.py 2021-01-08 
21:02:54.000000000 +0100
@@ -51,11 +51,11 @@
 def test_default_unauthn(app, client):
     """ Test default unauthn handler with and without json """
 
-    response = client.get("/multi_auth")
+    response = client.get("/profile")
     assert response.status_code == 302
-    assert response.headers["Location"] == 
"http://localhost/login?next=%2Fmulti_auth";
+    assert response.headers["Location"] == 
"http://localhost/login?next=%2Fprofile";
 
-    response = client.get("/multi_auth", headers={"Accept": 
"application/json"})
+    response = client.get("/profile", headers={"Accept": "application/json"})
     assert response.status_code == 401
     assert response.json["meta"]["code"] == 401
     # While "Basic" is acceptable, we never get a WWW-Authenticate header back 
since
@@ -67,11 +67,11 @@
 def test_default_unauthn_bp(app, client):
     """ Test default unauthn handler with blueprint prefix and login url """
 
-    response = client.get("/multi_auth")
+    response = client.get("/profile")
     assert response.status_code == 302
     assert (
         response.headers["Location"]
-        == "http://localhost/myprefix/mylogin?next=%2Fmulti_auth";
+        == "http://localhost/myprefix/mylogin?next=%2Fprofile";
     )
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Flask-Security-Too-3.4.3/tests/view_scaffold.py 
new/Flask-Security-Too-3.4.5/tests/view_scaffold.py
--- old/Flask-Security-Too-3.4.3/tests/view_scaffold.py 2020-06-13 
18:53:19.000000000 +0200
+++ new/Flask-Security-Too-3.4.5/tests/view_scaffold.py 2021-01-08 
21:02:54.000000000 +0100
@@ -29,6 +29,7 @@
 from flask.json import JSONEncoder
 from flask_security import (
     Security,
+    auth_required,
     current_user,
     login_required,
     SQLAlchemySessionUserDatastore,
@@ -170,6 +171,11 @@
             email=current_user.email,
         )
 
+    @app.route("/basicauth")
+    @auth_required("basic")
+    def basic():
+        return render_template_string("Basic auth success")
+
     return app
 
 

++++++ fix-dependencies.patch ++++++
--- /var/tmp/diff_new_pack.YlLHAO/_old  2021-07-08 22:49:36.031921997 +0200
+++ /var/tmp/diff_new_pack.YlLHAO/_new  2021-07-08 22:49:36.031921997 +0200
@@ -5,7 +5,7 @@
 @@ -14,20 +14,19 @@ with io.open("flask_security/__init__.py
  
  tests_require = [
-     "Flask-Mongoengine>=0.9.5",
+     "Flask-Mongoengine~=0.9.5",
 -    "peewee>=3.11.2",
 +    "peewee>=3.7.1",
      "Flask-SQLAlchemy>=2.3",
@@ -19,8 +19,8 @@
 +    "cryptography>=2.1.4",
      "isort>=4.2.2",
      "mock>=1.3.0",
-     "mongoengine>=0.15.3",
-     "mongomock>=3.14.0",
+     "mongoengine~=0.19.1",
+     "mongomock~=3.19.0",
      "msgcheck>=2.9",
 -    "pony>=0.7.11",
      "phonenumberslite>=8.11.1",
@@ -42,7 +42,7 @@
 @@ -13,20 +13,19 @@ Pallets-Sphinx-Themes>=1.2.0
  Sphinx>=1.8.5
  sphinx-issues>=1.2.0
- Flask-Mongoengine>=0.9.5
+ Flask-Mongoengine~=0.9.5
 -peewee>=3.11.2
 +peewee>=3.7.1
  Flask-SQLAlchemy>=2.3
@@ -56,8 +56,8 @@
 +cryptography>=2.1.4
  isort>=4.2.2
  mock>=1.3.0
- mongoengine>=0.15.3
- mongomock>=3.14.0
+ mongoengine~=0.19.1
+ mongomock~=3.19.0
  msgcheck>=2.9
 -pony>=0.7.11
  phonenumberslite>=8.11.1
@@ -73,7 +73,7 @@
  Pallets-Sphinx-Themes>=1.2.0
  Sphinx>=1.8.5
  sphinx-issues>=1.2.0
- Flask-Mongoengine>=0.9.5
+ Flask-Mongoengine~=0.9.5
 -peewee>=3.11.2
 +peewee>=3.7.1
  Flask-SQLAlchemy>=2.3
@@ -87,8 +87,8 @@
 +cryptography>=2.1.4
  isort>=4.2.2
  mock>=1.3.0
- mongoengine>=0.15.3
- mongomock>=3.14.0
+ mongoengine~=0.19.1
+ mongomock~=3.19.0
  msgcheck>=2.9
 -pony>=0.7.11
  phonenumberslite>=8.11.1
@@ -106,7 +106,7 @@
 @@ -83,20 +81,19 @@ sphinx-issues>=1.2.0
  
  [tests]
- Flask-Mongoengine>=0.9.5
+ Flask-Mongoengine~=0.9.5
 -peewee>=3.11.2
 +peewee>=3.7.1
  Flask-SQLAlchemy>=2.3
@@ -120,8 +120,8 @@
 +cryptography>=2.1.4
  isort>=4.2.2
  mock>=1.3.0
- mongoengine>=0.15.3
- mongomock>=3.14.0
+ mongoengine~=0.19.1
+ mongomock~=3.19.0
  msgcheck>=2.9
 -pony>=0.7.11
  phonenumberslite>=8.11.1

++++++ no-mongodb.patch ++++++
--- /var/tmp/diff_new_pack.YlLHAO/_old  2021-07-08 22:49:36.039921936 +0200
+++ /var/tmp/diff_new_pack.YlLHAO/_new  2021-07-08 22:49:36.043921905 +0200
@@ -1,8 +1,17 @@
-Index: Flask-Security-Too-3.4.0/tests/conftest.py
+Index: Flask-Security-Too-3.4.5/tests/conftest.py
 ===================================================================
---- Flask-Security-Too-3.4.0.orig/tests/conftest.py
-+++ Flask-Security-Too-3.4.0/tests/conftest.py
-@@ -617,7 +617,7 @@ def get_message(app):
+--- Flask-Security-Too-3.4.5.orig/tests/conftest.py
++++ Flask-Security-Too-3.4.5/tests/conftest.py
+@@ -607,7 +607,7 @@ def client_nc(request, sqlalchemy_app):
+     return app.test_client(use_cookies=False)
+ 
+ 
[email protected](params=["cl-sqlalchemy", "c2", "cl-mongo", "cl-peewee"])
[email protected](params=["cl-sqlalchemy", "c2", "cl-peewee"])
+ def clients(request, app, tmpdir, realdburl):
+     if request.param == "cl-sqlalchemy":
+         ds = sqlalchemy_setup(request, app, tmpdir, realdburl)
+@@ -645,7 +645,7 @@ def get_message(app):
  
  
  @pytest.fixture(

Reply via email to