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

Commit cbe88e7

Browse files
authored
Merge pull request #378 from cloudant/support-legacy-creds-with-iam
Added support for IAM API key in cloudant_bluemix method
2 parents 51fb2ae + 3e54fcd commit cbe88e7

File tree

8 files changed

+203
-42
lines changed

8 files changed

+203
-42
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
22

3+
- [IMPROVED] Added support for IAM API key in `cloudant_bluemix` method.
34
- [IMPROVED] Updated Travis CI and unit tests to run against CouchDB 2.1.1.
45

56
# 2.8.1 (2018-02-16)

src/cloudant/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def cloudant_bluemix(vcap_services, instance_name=None, service_name=None, **kwa
116116
"cloudantNoSQLDB": [
117117
{
118118
"credentials": {
119+
"apikey": "some123api456key"
119120
"username": "example",
120121
"password": "xxxxxxx",
121122
"host": "example.cloudant.com",

src/cloudant/_common_util.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2015, 2017 IBM Corp. All rights reserved.
2+
# Copyright (C) 2015, 2018 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.
@@ -23,7 +23,7 @@
2323
import json
2424

2525
from ._2to3 import LONGTYPE, STRTYPE, NONETYPE, UNITYPE, iteritems_
26-
from .error import CloudantArgumentError, CloudantException
26+
from .error import CloudantArgumentError, CloudantException, CloudantClientException
2727

2828
# Library Constants
2929

@@ -306,9 +306,14 @@ def __init__(self, vcap_services, instance_name=None, service_name=None):
306306
credentials = service['credentials']
307307
self._host = credentials['host']
308308
self._name = service.get('name')
309-
self._password = credentials['password']
310309
self._port = credentials.get('port', 443)
311310
self._username = credentials['username']
311+
if 'apikey' in credentials:
312+
self._iam_api_key = credentials['apikey']
313+
elif 'username' in credentials and 'password' in credentials:
314+
self._password = credentials['password']
315+
else:
316+
raise CloudantClientException(103)
312317
break
313318
else:
314319
raise CloudantException('Missing service in VCAP_SERVICES')
@@ -355,3 +360,8 @@ def url(self):
355360
def username(self):
356361
""" Return service username. """
357362
return self._username
363+
364+
@property
365+
def iam_api_key(self):
366+
""" Return service IAM API key. """
367+
return self._iam_api_key

src/cloudant/_messages.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
100: 'A general Cloudant client exception was raised.',
7373
101: 'Value must be set to a Database object. Found type: {0}',
7474
102: 'You must provide a url or an account.',
75+
103: 'Invalid service: IAM API key or username/password credentials are required.',
7576
404: 'Database {0} does not exist. Verify that the client is valid and try again.',
7677
412: 'Database {0} already exists.'
7778
}

src/cloudant/client.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (C) 2015, 2017 IBM Corp. All rights reserved.
2+
# Copyright (C) 2015, 2018 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.
@@ -29,7 +29,7 @@
2929
from .error import (
3030
CloudantArgumentError,
3131
CloudantClientException,
32-
CloudantDatabaseException)
32+
CloudantDatabaseException, CloudantException)
3333
from ._common_util import (
3434
USER_AGENT,
3535
append_response_error_content,
@@ -788,9 +788,17 @@ def bluemix(cls, vcap_services, instance_name=None, service_name=None, **kwargs)
788788
print client.all_dbs()
789789
"""
790790
service_name = service_name or 'cloudantNoSQLDB' # default service
791-
service = CloudFoundryService(vcap_services,
792-
instance_name=instance_name,
793-
service_name=service_name)
791+
try:
792+
service = CloudFoundryService(vcap_services,
793+
instance_name=instance_name,
794+
service_name=service_name)
795+
except CloudantException:
796+
raise CloudantClientException(103)
797+
798+
if hasattr(service, 'iam_api_key'):
799+
return Cloudant.iam(service.username,
800+
service.iam_api_key,
801+
url=service.url)
794802
return Cloudant(service.username,
795803
service.password,
796804
url=service.url,

tests/unit/client_tests.py

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -633,9 +633,9 @@ def test_cloudant_context_helper(self):
633633
self.fail('Exception {0} was raised.'.format(str(err)))
634634

635635
@skip_if_not_cookie_auth
636-
def test_cloudant_bluemix_context_helper(self):
636+
def test_cloudant_bluemix_context_helper_with_legacy_creds(self):
637637
"""
638-
Test that the cloudant_bluemix context helper works as expected.
638+
Test that the cloudant_bluemix context helper with legacy creds works as expected.
639639
"""
640640
instance_name = 'Cloudant NoSQL DB-lv'
641641
vcap_services = {'cloudantNoSQLDB': [{
@@ -657,6 +657,56 @@ def test_cloudant_bluemix_context_helper(self):
657657
except Exception as err:
658658
self.fail('Exception {0} was raised.'.format(str(err)))
659659

660+
@unittest.skipUnless(os.environ.get('IAM_API_KEY'),
661+
'Skipping Cloudant Bluemix context helper with IAM test')
662+
def test_cloudant_bluemix_context_helper_with_iam(self):
663+
"""
664+
Test that the cloudant_bluemix context helper with IAM works as expected.
665+
"""
666+
instance_name = 'Cloudant NoSQL DB-lv'
667+
vcap_services = {'cloudantNoSQLDB': [{
668+
'credentials': {
669+
'apikey': self.iam_api_key,
670+
'username': self.user,
671+
'host': '{0}.cloudant.com'.format(self.account),
672+
'port': 443,
673+
'url': self.url
674+
},
675+
'name': instance_name,
676+
}]}
677+
678+
try:
679+
with cloudant_bluemix(vcap_services, instance_name=instance_name) as c:
680+
self.assertIsInstance(c, Cloudant)
681+
self.assertIsInstance(c.r_session, requests.Session)
682+
except Exception as err:
683+
self.fail('Exception {0} was raised.'.format(str(err)))
684+
685+
def test_cloudant_bluemix_context_helper_raise_error_for_missing_iam_and_creds(self):
686+
"""
687+
Test that the cloudant_bluemix context helper raises a CloudantClientException
688+
when the IAM key, username, and password are missing in the VCAP_SERVICES env variable.
689+
"""
690+
instance_name = 'Cloudant NoSQL DB-lv'
691+
vcap_services = {'cloudantNoSQLDB': [{
692+
'credentials': {
693+
'host': '{0}.cloudant.com'.format(self.account),
694+
'port': 443,
695+
'url': self.url
696+
},
697+
'name': instance_name,
698+
}]}
699+
700+
try:
701+
with cloudant_bluemix(vcap_services, instance_name=instance_name) as c:
702+
self.assertIsInstance(c, Cloudant)
703+
self.assertIsInstance(c.r_session, requests.Session)
704+
except CloudantClientException as err:
705+
self.assertEqual(
706+
'Invalid service: IAM API key or username/password credentials are required.',
707+
str(err)
708+
)
709+
660710
def test_cloudant_bluemix_dedicated_context_helper(self):
661711
"""
662712
Test that the cloudant_bluemix context helper works as expected when
@@ -698,7 +748,7 @@ def test_constructor_with_account(self):
698748
)
699749

700750
@skip_if_not_cookie_auth
701-
def test_bluemix_constructor(self):
751+
def test_bluemix_constructor_with_legacy_creds(self):
702752
"""
703753
Test instantiating a client object using a VCAP_SERVICES environment
704754
variable.
@@ -730,7 +780,39 @@ def test_bluemix_constructor(self):
730780
finally:
731781
c.disconnect()
732782

733-
@skip_if_not_cookie_auth
783+
784+
@unittest.skipUnless(os.environ.get('IAM_API_KEY'),
785+
'Skipping Cloudant Bluemix constructor with IAM test')
786+
def test_bluemix_constructor_with_iam(self):
787+
"""
788+
Test instantiating a client object using a VCAP_SERVICES environment
789+
variable.
790+
"""
791+
instance_name = 'Cloudant NoSQL DB-lv'
792+
vcap_services = {'cloudantNoSQLDB': [{
793+
'credentials': {
794+
'apikey': self.iam_api_key,
795+
'username': self.user,
796+
'host': '{0}.cloudant.com'.format(self.account),
797+
'port': 443
798+
},
799+
'name': instance_name
800+
}]}
801+
802+
# create Cloudant Bluemix client
803+
c = Cloudant.bluemix(vcap_services)
804+
805+
try:
806+
c.connect()
807+
self.assertIsInstance(c, Cloudant)
808+
self.assertIsInstance(c.r_session, requests.Session)
809+
810+
except Exception as err:
811+
self.fail('Exception {0} was raised.'.format(str(err)))
812+
813+
finally:
814+
c.disconnect()
815+
734816
def test_bluemix_constructor_specify_instance_name(self):
735817
"""
736818
Test instantiating a client object using a VCAP_SERVICES environment
@@ -773,8 +855,7 @@ def test_bluemix_constructor_with_multiple_services(self):
773855
vcap_services = {'cloudantNoSQLDB': [
774856
{
775857
'credentials': {
776-
'username': self.user,
777-
'password': self.pwd,
858+
'apikey': '1234api',
778859
'host': '{0}.cloudant.com'.format(self.account),
779860
'port': 443,
780861
'url': self.url

0 commit comments

Comments
 (0)