Repository: fedora-infra/bodhi

Issue:
bodhi-client cannot handle expiring sessions
The bodhi client cannot work with expiring sessions. Once its session expires, it will never attempt to get a new session but will instead assume it can use the existing session. Thus, it does not matter if the user types the correct password or not:

```
$ time fedpkg update
Password:
Traceback (most recent call last):
  File "/usr/lib/python2.7/site-packages/bodhi/client/__init__.py", line 132, in new
    resp = client.save(**update)
  File "/usr/lib/python2.7/site-packages/bodhi/client/bindings.py", line 66, in wrapper
    result = method(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/bodhi/client/bindings.py", line 161, in save
    data=kwargs)
  File "/usr/lib/python2.7/site-packages/fedora/client/openidbaseclient.py", line 254, in send_request
    'output': to_bytes(output.text),
ServerError: ServerError(https://bodhi.fedoraproject.org/updates/, 403, Error returned from json module while processing https://bodhi.fedoraproject.org/updates/: Expecting value: line 6 column 1 (char 5)
```

# Workaround

Removing your cached sessions will work around the problem:

```
$ rm ~/.fedora/openidbaseclient-sessions.cache
```

Note that this workaround will only work until your new session expires, and then you will hit this problem again (and you can ```rm``` again to workaround once more). Fedora's Bodhi instance is configured to expire sessions after 24 hours.


Discussion:
Since this bug will be fixed in python-fedora, I am going to kick it off the 2.6.1 milestone and go ahead and release 2.6.1.

Resolving patch:
diff --git a/bodhi/client/bindings.py b/bodhi/client/bindings.py
--- a/bodhi/client/bindings.py
+++ b/bodhi/client/bindings.py
@@ -36,7 +36,7 @@
 
 import six
 
-from fedora.client import OpenIdBaseClient, FedoraClientError
+from fedora.client import AuthError, OpenIdBaseClient, FedoraClientError
 import fedora.client.openidproxyclient
 
 
@@ -66,7 +66,23 @@ def errorhandled(method):
     """ A decorator for BodhiClient that raises exceptions on failure. """
     @functools.wraps(method)
     def wrapper(*args, **kwargs):
-        result = method(*args, **kwargs)
+        try:
+            result = method(*args, **kwargs)
+        except AuthError:
+            # An AuthError can be raised for three different reasons:
+            #
+            # 0) The password is wrong.
+            # 1) The session cookies are expired. fedora.python does not handle this automatically.
+            # 2) The session cookies are not expired, but are no longer valid (for example, this can
+            #    happen if the server's auth secret has changed.)
+            #
+            # We don't know the difference between the cases here, but case #1 is fairly common and
+            # we can work around it and case #2 by removing the session cookies and csrf token and
+            # retrying the request. If the password is wrong, the second attempt will also fail but
+            # we won't guard it and the AuthError will still be raised.
+            args[0]._session.cookies.clear()
+            args[0].csrf_token = None
+            result = method(*args, **kwargs)
 
         if 'errors' not in result:
             return result


Test patch:
diff --git a/bodhi/tests/client/test_bindings.py b/bodhi/tests/client/test_bindings.py
--- a/bodhi/tests/client/test_bindings.py
+++ b/bodhi/tests/client/test_bindings.py
@@ -622,6 +622,66 @@ def im_gonna_fail_but_ill_be_cool_about_it(x, y, z=None):
 
         self.assertEqual(exc.exception.message, 'insert\ncoin(s)')
 
+    def test_retry_on_auth_failure_failure(self):
+        """
+        Test the decorator when the wrapped method raises an AuthError the second time it is run.
+
+        This test ensures that the decorator will give up after one retry if the wrapped method
+        raises a fedora.client.AuthError, raising the AuthError in question.
+
+        This test was written to assert the fix for
+        https://github.com/fedora-infra/bodhi/issues/1474
+        """
+        a_fake_self = mock.MagicMock()
+        a_fake_self.csrf_token = 'some_token'
+        a_fake_self.call_count = 0
+
+        @bindings.errorhandled
+        def wrong_password_lol(a_fake_self):
+            a_fake_self.call_count = a_fake_self.call_count + 1
+            raise fedora.client.AuthError('wrong password lol')
+
+        with self.assertRaises(fedora.client.AuthError) as exc:
+            # Wrong password always fails, so the second call should allow the Exception to be
+            # raised.
+            wrong_password_lol(a_fake_self)
+
+        self.assertEqual(exc.exception.message, 'wrong password lol')
+        a_fake_self._session.cookies.clear.assert_called_once_with()
+        self.assertTrue(a_fake_self.csrf_token is None)
+        self.assertEqual(a_fake_self.call_count, 2)
+
+    def test_retry_on_auth_failure_success(self):
+        """
+        Test the decorator when the wrapped method raises an AuthError the first time it is run.
+
+        This test ensures that the decorator will retry the wrapped method if it raises a
+        fedora.client.AuthError, after clearing cookies and the csrf token.
+
+        This test was written to assert the fix for
+        https://github.com/fedora-infra/bodhi/issues/1474
+        """
+        a_fake_self = mock.MagicMock()
+        a_fake_self.csrf_token = 'some_token'
+        a_fake_self.call_count = 0
+
+        @bindings.errorhandled
+        def wrong_password_lol(a_fake_self):
+            a_fake_self.call_count = a_fake_self.call_count + 1
+
+            # Fail on the first call with an AuthError to simulate bad session cookies.
+            if a_fake_self.call_count == 1:
+                raise fedora.client.AuthError('wrong password lol')
+
+            return 'here you go'
+
+        # No Exception should be raised.
+        wrong_password_lol(a_fake_self)
+
+        a_fake_self._session.cookies.clear.assert_called_once_with()
+        self.assertTrue(a_fake_self.csrf_token is None)
+        self.assertEqual(a_fake_self.call_count, 2)
+
     def test_success(self):
         """
         Test the decorator for the success case.