Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 6074fa3

Browse files
committed
- Handle auto renewal of session authentication data
- Sub-class requests.Session as InfiniteSession class and handle a 401 and 403 response as needed - Add kwarg to client to allow for auto renewal of session auth - Fix Cloudant auto connect bug
1 parent 68a1751 commit 6074fa3

File tree

3 files changed

+65
-6
lines changed

3 files changed

+65
-6
lines changed

src/cloudant/_2to3.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
if PY2:
3434
# pylint: disable=wrong-import-position,no-name-in-module,import-error,unused-import
3535
from urllib import quote as url_quote, quote_plus as url_quote_plus
36+
from urlparse import urlparse as url_parse
3637
from ConfigParser import RawConfigParser
3738

3839
def iteritems_(adict):
@@ -53,7 +54,9 @@ def next_(itr):
5354
"""
5455
return itr.next()
5556
else:
56-
from urllib.parse import quote as url_quote, quote_plus as url_quote_plus # pylint: disable=wrong-import-position,no-name-in-module,import-error,ungrouped-imports
57+
from urllib.parse import urlparse as url_parse # pylint: disable=wrong-import-position,no-name-in-module,import-error,ungrouped-imports
58+
from urllib.parse import quote as url_quote # pylint: disable=wrong-import-position,no-name-in-module,import-error,ungrouped-imports
59+
from urllib.parse import quote_plus as url_quote_plus # pylint: disable=wrong-import-position,no-name-in-module,import-error,ungrouped-imports
5760
from configparser import RawConfigParser # pylint: disable=wrong-import-position,no-name-in-module,import-error
5861

5962
def iteritems_(adict):

src/cloudant/_common_util.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
import platform
2222
from collections import Sequence
2323
import json
24+
from requests import Session
2425

25-
from ._2to3 import STRTYPE, NONETYPE, UNITYPE, iteritems_
26+
from ._2to3 import STRTYPE, NONETYPE, UNITYPE, iteritems_, url_parse
2627
from .error import CloudantArgumentError
2728

2829
# Library Constants
@@ -288,3 +289,40 @@ class _Code(str):
288289
"""
289290
def __new__(cls, code):
290291
return str.__new__(cls, code)
292+
293+
class InfiniteSession(Session):
294+
"""
295+
This class provides for the ability to automatically renew session login
296+
information in the event of expired session authentication.
297+
"""
298+
299+
def __init__(self, username, password, server_url):
300+
super(InfiniteSession, self).__init__()
301+
self._username = username
302+
self._password = password
303+
self._server_url = server_url
304+
305+
def request(self, method, url, **kwargs):
306+
"""
307+
Overrides ``requests.Session.request`` to perform a POST to the
308+
_session endpoint to renew Session cookie authentication settings and
309+
then retry the original request, if necessary.
310+
"""
311+
resp = super(InfiniteSession, self).request(method, url, **kwargs)
312+
path = url_parse(url).path.lower()
313+
post_to_session = method.upper() == 'POST' and path == '/_session'
314+
is_expired = any((
315+
resp.status_code == 403 and
316+
resp.json().get('error') == 'credentials_expired',
317+
resp.status_code == 401
318+
))
319+
if not post_to_session and is_expired:
320+
super(InfiniteSession, self).request(
321+
'POST',
322+
'/'.join([self._server_url, '_session']),
323+
data={'name': self._username, 'password': self._password},
324+
headers={'Content-Type': 'application/x-www-form-urlencoded'}
325+
)
326+
resp = super(InfiniteSession, self).request(method, url, **kwargs)
327+
328+
return resp

src/cloudant/client.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
from .database import CloudantDatabase, CouchDatabase
2626
from .feed import Feed, InfiniteFeed
2727
from .error import CloudantException, CloudantArgumentError
28-
from ._common_util import USER_AGENT, append_response_error_content
28+
from ._common_util import (
29+
USER_AGENT,
30+
append_response_error_content,
31+
InfiniteSession
32+
)
2933

3034

3135
class CouchDB(dict):
@@ -49,6 +53,9 @@ class CouchDB(dict):
4953
configuring requests.
5054
:param bool connect: Keyword argument, if set to True performs the call to
5155
connect as part of client construction. Default is False.
56+
:param bool auto_renew: Keyword argument, if set to True performs
57+
automatic renewal of expired session authentication settings.
58+
Default is False.
5259
"""
5360
_DATABASE_CLASS = CouchDatabase
5461

@@ -63,7 +70,9 @@ def __init__(self, user, auth_token, admin_party=False, **kwargs):
6370
self.encoder = kwargs.get('encoder') or json.JSONEncoder
6471
self.adapter = kwargs.get('adapter')
6572
self.r_session = None
66-
if kwargs.get('connect', False):
73+
self._auto_renew = kwargs.get('auto_renew', False)
74+
connect_to_couch = kwargs.get('connect', False)
75+
if connect_to_couch and self._DATABASE_CLASS == CouchDatabase:
6776
self.connect()
6877

6978
def connect(self):
@@ -74,7 +83,14 @@ def connect(self):
7483
if self.r_session:
7584
return
7685

77-
self.r_session = requests.Session()
86+
if self._auto_renew and not self.admin_party:
87+
self.r_session = InfiniteSession(
88+
self._user,
89+
self._auth_token,
90+
self.server_url
91+
)
92+
else:
93+
self.r_session = requests.Session()
7894
# If a Transport Adapter was supplied add it to the session
7995
if self.adapter is not None:
8096
self.r_session.mount(self.server_url, self.adapter)
@@ -398,7 +414,6 @@ class Cloudant(CouchDB):
398414

399415
def __init__(self, cloudant_user, auth_token, **kwargs):
400416
super(Cloudant, self).__init__(cloudant_user, auth_token, **kwargs)
401-
402417
self._client_user_header = {'User-Agent': USER_AGENT}
403418
account = kwargs.get('account')
404419
url = kwargs.get('url')
@@ -413,6 +428,9 @@ def __init__(self, cloudant_user, auth_token, **kwargs):
413428
if self.server_url is None:
414429
raise CloudantException('You must provide a url or an account.')
415430

431+
if kwargs.get('connect', False):
432+
self.connect()
433+
416434
def db_updates(self, raw_data=False, **kwargs):
417435
"""
418436
Returns the ``_db_updates`` feed iterator. The ``_db_updates`` feed can

0 commit comments

Comments
 (0)