11#!/usr/bin/env python
2- # Copyright (c) 2015, 2016, 2017 IBM Corp. All rights reserved.
2+ # Copyright (c) 2015, 2017 IBM Corp. All rights reserved.
33#
44# Licensed under the Apache License, Version 2.0 (the "License");
55# you may not use this file except in compliance with the License.
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_join
2728from .error import CloudantArgumentError , CloudantException
2829
2930# Library Constants
@@ -276,6 +277,7 @@ def append_response_error_content(response, **kwargs):
276277
277278# Classes
278279
280+
279281class _Code (str ):
280282 """
281283 Wraps a ``str`` object as a _Code object providing the means to handle
@@ -287,66 +289,212 @@ def __new__(cls, code):
287289 return str .__new__ (cls , code .encode ('utf8' ))
288290 return str .__new__ (cls , code )
289291
290- class InfiniteSession (Session ):
292+
293+ class ClientSession (Session ):
294+ """
295+ This class extends Session and provides a default timeout.
296+ """
297+
298+ def __init__ (self , ** kwargs ):
299+ super (ClientSession , self ).__init__ ()
300+ self ._timeout = kwargs .get ('timeout' , None )
301+
302+ def request (self , method , url , ** kwargs ): # pylint: disable=W0221
303+ """
304+ Overrides ``requests.Session.request`` to set the timeout.
305+ """
306+ resp = super (ClientSession , self ).request (
307+ method , url , timeout = self ._timeout , ** kwargs )
308+
309+ return resp
310+
311+
312+ class CookieSession (ClientSession ):
291313 """
292- This class provides for the ability to automatically renew session login
293- information in the event of expired session authentication.
314+ This class extends ClientSession and provides cookie authentication.
294315 """
295316
296317 def __init__ (self , username , password , server_url , ** kwargs ):
297- super (InfiniteSession , self ).__init__ ()
318+ super (CookieSession , self ).__init__ (** kwargs )
298319 self ._username = username
299320 self ._password = password
300- self ._server_url = server_url
301- self ._timeout = kwargs .get ('timeout' , None )
321+ self ._auto_renew = kwargs .get ('auto_renew' , False )
322+ self ._session_url = url_join (server_url , '_session' )
323+
324+ def info (self ):
325+ """
326+ Get cookie based login user information.
327+ """
328+ resp = self .get (self ._session_url )
329+ resp .raise_for_status ()
330+
331+ return resp .json ()
332+
333+ def login (self ):
334+ """
335+ Perform cookie based user login.
336+ """
337+ resp = super (CookieSession , self ).request (
338+ 'POST' ,
339+ self ._session_url ,
340+ data = {'name' : self ._username , 'password' : self ._password },
341+ )
342+ resp .raise_for_status ()
343+
344+ def logout (self ):
345+ """
346+ Logout cookie based user.
347+ """
348+ resp = super (CookieSession , self ).request ('DELETE' , self ._session_url )
349+ resp .raise_for_status ()
302350
303351 def request (self , method , url , ** kwargs ): # pylint: disable=W0221
304352 """
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.
353+ Overrides ``requests.Session.request`` to renew the cookie and then
354+ retry the original request (if required).
308355 """
309- resp = super (InfiniteSession , self ).request (
310- method , url , timeout = self ._timeout , ** kwargs )
311- path = url_parse (url ).path .lower ()
312- post_to_session = method .upper () == 'POST' and path == '/_session'
356+ resp = super (CookieSession , self ).request (method , url , ** kwargs )
357+
358+ if not self ._auto_renew :
359+ return resp
360+
313361 is_expired = any ((
314362 resp .status_code == 403 and
315363 resp .json ().get ('error' ) == 'credentials_expired' ,
316364 resp .status_code == 401
317365 ))
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 )
366+
367+ if is_expired :
368+ self .login ()
369+ resp = super (CookieSession , self ).request (method , url , ** kwargs )
327370
328371 return resp
329372
330- class ClientSession (Session ):
373+ def set_credentials (self , username , password ):
374+ """
375+ Set a new username and password.
376+
377+ :param str username: New username.
378+ :param str password: New password.
379+ """
380+ if username is not None :
381+ self ._username = username
382+
383+ if password is not None :
384+ self ._password = password
385+
386+
387+ class IAMSession (ClientSession ):
331388 """
332- This class extends Session and provides a default timeout .
389+ This class extends ClientSession and provides IAM authentication .
333390 """
334391
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 )
392+ def __init__ (self , api_key , server_url , ** kwargs ):
393+ super (IAMSession , self ).__init__ (** kwargs )
394+ self ._api_key = api_key
395+ self ._auto_renew = kwargs .get ('auto_renew' , False )
396+ self ._session_url = url_join (server_url , '_iam_session' )
397+ self ._token_url = os .environ .get (
398+ 'IAM_TOKEN_URL' , 'https://iam.bluemix.net/oidc/token' )
399+
400+ def info (self ):
401+ """
402+ Get IAM cookie based login user information.
403+ """
404+ resp = self .get (self ._session_url )
405+ resp .raise_for_status ()
406+
407+ return resp .json ()
408+
409+ def login (self ):
410+ """
411+ Perform IAM cookie based user login.
412+ """
413+ access_token = self ._get_access_token ()
414+ try :
415+ super (IAMSession , self ).request (
416+ 'POST' ,
417+ self ._session_url ,
418+ headers = {'Content-Type' : 'application/json' },
419+ data = json .dumps ({'access_token' : access_token })
420+ ).raise_for_status ()
421+
422+ except RequestException :
423+ raise CloudantException (
424+ 'Failed to exchange IAM token with Cloudant' )
425+
426+ def logout (self ):
427+ """
428+ Logout IAM cookie based user.
429+ """
430+ self .cookies .clear ()
341431
342432 def request (self , method , url , ** kwargs ): # pylint: disable=W0221
343433 """
344- Overrides ``requests.Session.request`` to set the timeout.
434+ Overrides ``requests.Session.request`` to renew the IAM cookie
435+ and then retry the original request (if required).
345436 """
346- resp = super (ClientSession , self ).request (
347- method , url , timeout = self ._timeout , ** kwargs )
437+ # The CookieJar API prevents callers from getting an individual Cookie
438+ # object by name.
439+ # We are forced to use the only exposed method of discarding expired
440+ # cookies from the CookieJar. Internally this involves iterating over
441+ # the entire CookieJar and calling `.is_expired()` on each Cookie
442+ # object.
443+ self .cookies .clear_expired_cookies ()
444+
445+ if self ._auto_renew and 'IAMSession' not in self .cookies .keys ():
446+ self .login ()
447+
448+ resp = super (IAMSession , self ).request (method , url , ** kwargs )
449+
450+ if not self ._auto_renew :
451+ return resp
452+
453+ if resp .status_code == 401 :
454+ self .login ()
455+ resp = super (IAMSession , self ).request (method , url , ** kwargs )
456+
348457 return resp
349458
459+ def set_credentials (self , username , api_key ):
460+ """
461+ Set a new IAM API key.
462+
463+ :param str username: Username parameter is unused.
464+ :param str api_key: New IAM API key.
465+ """
466+ if api_key is not None :
467+ self ._api_key = api_key
468+
469+ def _get_access_token (self ):
470+ """
471+ Get IAM access token using API key.
472+ """
473+ err = 'Failed to contact IAM token service'
474+ try :
475+ resp = super (IAMSession , self ).request (
476+ 'POST' ,
477+ self ._token_url ,
478+ auth = ('bx' , 'bx' ), # required for user API keys
479+ headers = {'Accepts' : 'application/json' },
480+ data = {
481+ 'grant_type' : 'urn:ibm:params:oauth:grant-type:apikey' ,
482+ 'response_type' : 'cloud_iam' ,
483+ 'apikey' : self ._api_key
484+ }
485+ )
486+ err = resp .json ().get ('errorMessage' , err )
487+ resp .raise_for_status ()
488+
489+ return resp .json ()['access_token' ]
490+
491+ except KeyError :
492+ raise CloudantException ('Invalid response from IAM token service' )
493+
494+ except RequestException :
495+ raise CloudantException (err )
496+
497+
350498class CloudFoundryService (object ):
351499 """ Manages Cloud Foundry service configuration. """
352500
0 commit comments