1717throughout the library.
1818"""
1919
20+ import os
2021import sys
2122import platform
2223from collections import Sequence
2324import json
24- from requests import Session
25+ from requests import RequestException , Session
2526
26- from ._2to3 import LONGTYPE , STRTYPE , NONETYPE , UNITYPE , iteritems_ , url_parse
27+ from ._2to3 import LONGTYPE , STRTYPE , NONETYPE , UNITYPE , iteritems_ , url_parse , \
28+ url_join
2729from .error import CloudantArgumentError , CloudantException
2830
2931# Library Constants
@@ -276,6 +278,7 @@ def append_response_error_content(response, **kwargs):
276278
277279# Classes
278280
281+
279282class _Code (str ):
280283 """
281284 Wraps a ``str`` object as a _Code object providing the means to handle
@@ -287,66 +290,187 @@ def __new__(cls, code):
287290 return str .__new__ (cls , code .encode ('utf8' ))
288291 return str .__new__ (cls , code )
289292
290- class InfiniteSession (Session ):
293+
294+ class ClientSession (Session ):
291295 """
292- This class provides for the ability to automatically renew session login
293- information in the event of expired session authentication.
296+ This class extends Session and provides a default timeout.
297+ """
298+
299+ def __init__ (self , ** kwargs ):
300+ super (ClientSession , self ).__init__ ()
301+ self ._timeout = kwargs .get ('timeout' , None )
302+
303+ def request (self , method , url , ** kwargs ): # pylint: disable=W0221
304+ """
305+ Overrides ``requests.Session.request`` to set the timeout.
306+ """
307+ resp = super (ClientSession , self ).request (
308+ method , url , timeout = self ._timeout , ** kwargs )
309+
310+ return resp
311+
312+
313+ class CookieSession (ClientSession ):
314+ """
315+ This class extends ClientSession and provides cookie authentication.
294316 """
295317
296318 def __init__ (self , username , password , server_url , ** kwargs ):
297- super (InfiniteSession , self ).__init__ ()
319+ super (CookieSession , self ).__init__ (** kwargs )
298320 self ._username = username
299321 self ._password = password
300- self ._server_url = server_url
301- self ._timeout = kwargs .get ('timeout' , None )
322+ self ._auto_renew = kwargs .get ('auto_renew' , False )
323+ self ._session_url = url_join (server_url , '_session' )
324+
325+ def info (self ):
326+ """
327+ Get cookie based login user information.
328+ """
329+ resp = self .get (self ._session_url )
330+ resp .raise_for_status ()
331+
332+ return resp .json ()
333+
334+ def login (self ):
335+ """
336+ Perform cookie based user login.
337+ """
338+ resp = super (CookieSession , self ).request (
339+ 'POST' ,
340+ self ._session_url ,
341+ data = {'name' : self ._username , 'password' : self ._password },
342+ )
343+ resp .raise_for_status ()
344+
345+ def logout (self ):
346+ """
347+ Logout cookie based user.
348+ """
349+ resp = super (CookieSession , self ).request ('DELETE' , self ._session_url )
350+ resp .raise_for_status ()
302351
303352 def request (self , method , url , ** kwargs ): # pylint: disable=W0221
304353 """
305- Overrides ``requests.Session.request`` to perform a POST to the
306- _session endpoint to renew Session cookie authentication settings and
307- then retry the original request, if necessary.
354+ Overrides ``requests.Session.request`` to renew the cookie and then
355+ retry the original request (if required).
308356 """
309- resp = super (InfiniteSession , self ).request (
310- method , url , timeout = self . _timeout , ** kwargs )
357+ resp = super (CookieSession , self ).request (method , url , ** kwargs )
358+
311359 path = url_parse (url ).path .lower ()
312360 post_to_session = method .upper () == 'POST' and path == '/_session'
361+
362+ if not self ._auto_renew or post_to_session :
363+ return resp
364+
313365 is_expired = any ((
314366 resp .status_code == 403 and
315367 resp .json ().get ('error' ) == 'credentials_expired' ,
316368 resp .status_code == 401
317369 ))
318- if not post_to_session and is_expired :
319- super (InfiniteSession , self ).request (
320- 'POST' ,
321- '/' .join ([self ._server_url , '_session' ]),
322- data = {'name' : self ._username , 'password' : self ._password },
323- headers = {'Content-Type' : 'application/x-www-form-urlencoded' }
324- )
325- resp = super (InfiniteSession , self ).request (
326- method , url , timeout = self ._timeout , ** kwargs )
370+
371+ if is_expired :
372+ self .login ()
373+ resp = super (CookieSession , self ).request (method , url , ** kwargs )
327374
328375 return resp
329376
330- class ClientSession (Session ):
377+
378+ class IAMSession (ClientSession ):
331379 """
332- This class extends Session and provides a default timeout .
380+ This class extends ClientSession and provides IAM authentication .
333381 """
334382
335- def __init__ (self , username , password , server_url , ** kwargs ):
336- super (ClientSession , self ).__init__ ()
337- self ._username = username
338- self ._password = password
339- self ._server_url = server_url
340- self ._timeout = kwargs .get ('timeout' , None )
383+ def __init__ (self , api_key , server_url , ** kwargs ):
384+ super (IAMSession , self ).__init__ (** kwargs )
385+ self ._api_key = api_key
386+ self ._auto_renew = kwargs .get ('auto_renew' , False )
387+ self ._session_url = url_join (server_url , '_iam_session' )
388+ self ._token_url = os .environ .get (
389+ 'IAM_TOKEN_URL' , 'https://iam.bluemix.net/oidc/token' )
390+
391+ def info (self ):
392+ """
393+ Get IAM cookie based login user information.
394+ """
395+ resp = self .get (self ._session_url )
396+ resp .raise_for_status ()
397+
398+ return resp .json ()
399+
400+ def login (self ):
401+ """
402+ Perform IAM cookie based user login.
403+ """
404+ access_token = self ._get_access_token ()
405+ try :
406+ super (IAMSession , self ).request (
407+ 'POST' ,
408+ self ._session_url ,
409+ headers = {'Content-Type' : 'application/json' },
410+ data = json .dumps ({'access_token' : access_token })
411+ ).raise_for_status ()
412+
413+ except RequestException :
414+ raise CloudantException (
415+ 'Failed to exchange IAM token with Cloudant' )
416+
417+ def logout (self ):
418+ """
419+ Logout IAM cookie based user.
420+ """
421+ self .cookies .clear ()
341422
342423 def request (self , method , url , ** kwargs ): # pylint: disable=W0221
343424 """
344- Overrides ``requests.Session.request`` to set the timeout.
425+ Overrides ``requests.Session.request`` to renew the IAM cookie
426+ and then retry the original request (if required).
345427 """
346- resp = super (ClientSession , self ).request (
347- method , url , timeout = self ._timeout , ** kwargs )
428+ resp = super (IAMSession , self ).request (method , url , ** kwargs )
429+
430+ if not self ._auto_renew or url in [self ._session_url , self ._token_url ]:
431+ return resp
432+
433+ is_expired = any ((
434+ resp .status_code == 403 and
435+ resp .json ().get ('error' ) == 'credentials_expired' ,
436+ resp .status_code == 401
437+ ))
438+
439+ if is_expired :
440+ self .login ()
441+ resp = super (IAMSession , self ).request (method , url , ** kwargs )
442+
348443 return resp
349444
445+ def _get_access_token (self ):
446+ """
447+ Get IAM access token using API key.
448+ """
449+ err = 'Failed to contact IAM token service'
450+ try :
451+ resp = super (IAMSession , self ).request (
452+ 'POST' ,
453+ self ._token_url ,
454+ auth = ('bx' , 'bx' ), # required for user API keys
455+ headers = {'Accepts' : 'application/json' },
456+ data = {
457+ 'grant_type' : 'urn:ibm:params:oauth:grant-type:apikey' ,
458+ 'response_type' : 'cloud_iam' ,
459+ 'apikey' : self ._api_key
460+ }
461+ )
462+ err = resp .json ().get ('errorMessage' , err )
463+ resp .raise_for_status ()
464+
465+ return resp .json ()['access_token' ]
466+
467+ except KeyError :
468+ raise CloudantException ('Invalid response from IAM token service' )
469+
470+ except RequestException :
471+ raise CloudantException (err )
472+
473+
350474class CloudFoundryService (object ):
351475 """ Manages Cloud Foundry service configuration. """
352476
0 commit comments