Skip to content

Commit 2a0853b

Browse files
authored
Merge develop into master (#460)
* doc(IAM): Document changes for IAM support * Add support for IAM (#456) * Feat(IAM): Adding IAM feature * Update constructors for IAM support (#459) * :feat(iam): Generate service constructors to support IAM
1 parent b53b30d commit 2a0853b

19 files changed

Lines changed: 618 additions & 70 deletions

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,26 @@ The [examples][examples] folder has basic and advanced examples.
4747

4848
Service credentials are required to access the APIs.
4949

50-
If you run your app in IBM Cloud, you don't need to specify the username and password. In that case, the SDK uses the `VCAP_SERVICES` environment variable to load the credentials.
51-
52-
To run locally or outside of IBM Cloud you need the `username` and `password` credentials for each service. (Service credentials are different from your IBM Cloud account email and password.)
50+
If you run your app in IBM Cloud, you don't need to specify the username and password or IAM API key (`apikey`). In that case, the SDK uses the `VCAP_SERVICES` environment variable to load the credentials.
51+
To run locally or outside of IBM Cloud you need the `username` and `password` credentials or IAM API key (`apikey`) for each service. (Service credentials are different from your IBM Cloud account email and password.)
5352

5453
To create an instance of the service:
5554

5655
1. Log in to [IBM Cloud][ibm_cloud].
5756
1. Create an instance of the service:
57+
1. Click on **Create Resource**.
5858
1. In the IBM Cloud **Catalog**, select the Watson service you want to use. For example, select the Conversation service.
5959
1. Type a unique name for the service instance in the **Service name** field. For example, type `my-service-name`. Leave the default values for the other options.
6060
1. Click **Create**.
6161

6262
To get your service credentials:
6363

64-
Copy your credentials from the **Service details** page. To find the the Service details page for an existing service, navigate to your IBM Cloud dashboard and click the service name.
64+
Copy your credentials from the **Manage** page. To find the Service details page for an existing service, navigate to your [IBM Cloud][ibm_cloud] dashboard and click the service name.
6565

66-
1. On the **Service Details** page, click **Service Credentials**, and then **View credentials**.
67-
1. Copy `username`, `password`, and `url`.
66+
1. On the **Manage** page, you will see a **Credentials** pane
67+
1. Depending on the service you will see use either:
68+
* 2.a: `username`, `password`, and `url`(optional).
69+
* 2.b: `apikey` which is the value for parameter `iam_api_key` when initializing the constructor.
6870

6971
## Python Version
7072

test/integration/test_discovery_v1.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,24 @@
88
@pytest.mark.skipif(
99
os.getenv('VCAP_SERVICES') is None, reason='requires VCAP_SERVICES')
1010
class Discoveryv1(TestCase):
11-
discovery = None
12-
environment_id = 'e15f6424-f887-4f50-b4ea-68267c36fc9c' # This environment is created for integration testing
13-
collection_id = None
14-
15-
@classmethod
16-
def setup_class(cls):
17-
cls.discovery = watson_developer_cloud.DiscoveryV1(
11+
def setUp(self):
12+
self.discovery = watson_developer_cloud.DiscoveryV1(
1813
version='2017-10-16',
1914
username="YOUR SERVICE USERNAME",
2015
password="YOUR SERVICE PASSWORD")
21-
cls.discovery.set_default_headers({
16+
self.discovery.set_default_headers({
2217
'X-Watson-Learning-Opt-Out': '1',
2318
'X-Watson-Test': '1'
2419
})
25-
cls.collection_id = cls.discovery.list_collections(cls.environment_id)['collections'][0]['collection_id']
20+
self.environment_id = 'e15f6424-f887-4f50-b4ea-68267c36fc9c' # This environment is created for integration testing
21+
collections = self.discovery.list_collections(self.environment_id)['collections']
22+
self.collection_id = collections[0]['collection_id']
2623

27-
@classmethod
28-
def teardown_class(cls):
29-
collections = cls.discovery.list_collections(cls.environment_id)['collections']
24+
def tearDown(self):
25+
collections = self.discovery.list_collections(self.environment_id)['collections']
3026
for collection in collections:
3127
if collection['name'] != 'DO-NOT-DELETE':
32-
cls.discovery.delete_collection(cls.environment_id, collection['collection_id'])
28+
self.discovery.delete_collection(self.environment_id, collection['collection_id'])
3329

3430
def test_environments(self):
3531
envs = self.discovery.list_environments()

test/integration/test_speech_to_text_v1.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ class TestSpeechToTextV1(TestCase):
1616
@classmethod
1717
def setup_class(cls):
1818
cls.speech_to_text = watson_developer_cloud.SpeechToTextV1(
19-
username=os.getenv('YOUR SERVICE USERNAME'),
20-
password=os.getenv('YOUR SERVICE PASSWORD'))
19+
username='YOUR SERVICE USERNAME',
20+
password='YOUR SERVICE PASSWORD')
2121
cls.speech_to_text.set_default_headers({
2222
'X-Watson-Learning-Opt-Out':
2323
'1',

test/integration/test_visual_recognition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class IntegrationTestVisualRecognitionV3(TestCase):
1616
@classmethod
1717
def setup_class(cls):
1818
cls.visual_recognition = watson_developer_cloud.VisualRecognitionV3(
19-
'2016-05-20', api_key=os.environ.get('YOUR API KEY'))
19+
'2016-05-20', api_key='YOUR API KEY')
2020
cls.visual_recognition.set_default_headers({
2121
'X-Watson-Learning-Opt-Out':
2222
'1',
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import responses
2+
from watson_developer_cloud import iam_token_manager
3+
import time
4+
5+
@responses.activate
6+
def test_request_token():
7+
iam_url = "https://iam.bluemix.net/identity/token"
8+
response = """{
9+
"access_token": "oAeisG8yqPY7sFR_x66Z15",
10+
"token_type": "Bearer",
11+
"expires_in": 3600,
12+
"expiration": 1524167011,
13+
"refresh_token": "jy4gl91BQ"
14+
}"""
15+
responses.add(responses.POST, url=iam_url, body=response, status=200)
16+
17+
token_manager = iam_token_manager.IAMTokenManager("iam_api_key", "iam_access_token", iam_url)
18+
token_manager._request_token()
19+
20+
assert responses.calls[0].request.url == iam_url
21+
assert responses.calls[0].response.text == response
22+
assert len(responses.calls) == 1
23+
24+
@responses.activate
25+
def test_refresh_token():
26+
iam_url = "https://iam.bluemix.net/identity/token"
27+
response = """{
28+
"access_token": "oAeisG8yqPY7sFR_x66Z15",
29+
"token_type": "Bearer",
30+
"expires_in": 3600,
31+
"expiration": 1524167011,
32+
"refresh_token": "jy4gl91BQ"
33+
}"""
34+
responses.add(responses.POST, url=iam_url, body=response, status=200)
35+
36+
token_manager = iam_token_manager.IAMTokenManager("iam_api_key", "iam_access_token", iam_url)
37+
token_manager._refresh_token()
38+
39+
assert responses.calls[0].request.url == iam_url
40+
assert responses.calls[0].response.text == response
41+
assert len(responses.calls) == 1
42+
43+
@responses.activate
44+
def test_is_token_expired():
45+
token_manager = iam_token_manager.IAMTokenManager("iam_api_key", "iam_access_token", "iam_url")
46+
token_manager.token_info = {
47+
"access_token": "oAeisG8yqPY7sFR_x66Z15",
48+
"token_type": "Bearer",
49+
"expires_in": 3600,
50+
"expiration": int(time.time()) + 6000,
51+
"refresh_token": "jy4gl91BQ"
52+
}
53+
assert token_manager._is_token_expired() is False
54+
token_manager.token_info['expiration'] = int(time.time()) - 3600
55+
assert token_manager._is_token_expired()
56+
57+
@responses.activate
58+
def test_is_refresh_token_expired():
59+
token_manager = iam_token_manager.IAMTokenManager("iam_api_key", "iam_access_token", "iam_url")
60+
token_manager.token_info = {
61+
"access_token": "oAeisG8yqPY7sFR_x66Z15",
62+
"token_type": "Bearer",
63+
"expires_in": 3600,
64+
"expiration": int(time.time()),
65+
"refresh_token": "jy4gl91BQ"
66+
}
67+
assert token_manager._is_refresh_token_expired() is False
68+
token_manager.token_info['expiration'] = int(time.time()) - (8 * 24 * 3600)
69+
assert token_manager._is_token_expired()
70+
71+
@responses.activate
72+
def test_get_token():
73+
iam_url = "https://iam.bluemix.net/identity/token"
74+
token_manager = iam_token_manager.IAMTokenManager("iam_api_key", iam_url=iam_url)
75+
token_manager.user_access_token = 'user_access_token'
76+
77+
# Case 1:
78+
token = token_manager.get_token()
79+
assert token == token_manager.user_access_token
80+
81+
# Case 2:
82+
token_manager.user_access_token = ''
83+
response = """{
84+
"access_token": "hellohello",
85+
"token_type": "Bearer",
86+
"expires_in": 3600,
87+
"expiration": 1524167011,
88+
"refresh_token": "jy4gl91BQ"
89+
}"""
90+
responses.add(responses.POST, url=iam_url, body=response, status=200)
91+
token = token_manager.get_token()
92+
assert token == "hellohello"
93+
94+
# Case 3:
95+
token_manager.token_info['expiration'] = int(time.time()) - (20 * 24 * 3600)
96+
token = token_manager.get_token()
97+
assert "grant_type=urn" in responses.calls[1].request.body
98+
token_manager.token_info['expiration'] = int(time.time()) - 4000
99+
token = token_manager.get_token()
100+
assert "grant_type=refresh_token" in responses.calls[2].request.body
101+
102+
# Case 4
103+
token_manager.token_info = {
104+
"access_token": "dummy",
105+
"token_type": "Bearer",
106+
"expires_in": 3600,
107+
"expiration": int(time.time()) + 3600,
108+
"refresh_token": "jy4gl91BQ"
109+
}
110+
token = token_manager.get_token()
111+
assert token == 'dummy'

test/unit/test_watson_service.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import pytest
44
from watson_developer_cloud import WatsonService
5+
import time
56

67
import responses
78

@@ -12,14 +13,20 @@
1213
class AnyServiceV1(WatsonService):
1314
default_url = 'https://gateway.watsonplatform.net/test/api'
1415

15-
def __init__(self, version, url=default_url, username=None, password=None):
16+
def __init__(self, version, url=default_url, username=None, password=None,
17+
iam_api_key=None,
18+
iam_access_token=None,
19+
iam_url=None):
1620
WatsonService.__init__(
1721
self,
1822
vcap_services_name='test',
1923
url=url,
2024
username=username,
2125
password=password,
22-
use_vcap_services=True)
26+
use_vcap_services=True,
27+
iam_api_key=iam_api_key,
28+
iam_access_token=iam_access_token,
29+
iam_url=iam_url)
2330
self.version = version
2431

2532
def op_with_path_params(self, path0, path1):
@@ -39,6 +46,10 @@ def with_http_config(self, http_config):
3946
response = self.request(method='GET', url='', accept_json=True)
4047
return response
4148

49+
def any_service_call(self):
50+
response = self.request(method='GET', url='', accept_json=True)
51+
return response
52+
4253
@responses.activate
4354
def test_url_encoding():
4455
service = AnyServiceV1('2017-07-07', username='username', password='password')
@@ -84,3 +95,31 @@ def test_fail_http_config():
8495
service = AnyServiceV1('2017-07-07', username='username', password='password')
8596
with pytest.raises(TypeError):
8697
service.with_http_config(None)
98+
99+
@responses.activate
100+
def test_iam():
101+
iam_url = "https://iam.bluemix.net/identity/token"
102+
service = AnyServiceV1('2017-07-07', iam_api_key="iam_api_key")
103+
assert service.token_manager is not None
104+
105+
service.token_manager.token_info = {
106+
"access_token": "dummy",
107+
"token_type": "Bearer",
108+
"expires_in": 3600,
109+
"expiration": int(time.time()) - 4000,
110+
"refresh_token": "jy4gl91BQ"
111+
}
112+
response = """{
113+
"access_token": "hellohello",
114+
"token_type": "Bearer",
115+
"expires_in": 3600,
116+
"expiration": 1524167011,
117+
"refresh_token": "jy4gl91BQ"
118+
}"""
119+
responses.add(responses.POST, url=iam_url, body=response, status=200)
120+
responses.add(responses.GET,
121+
service.default_url,
122+
body=json.dumps({"foobar": "baz"}),
123+
content_type='application/json')
124+
service.any_service_call()
125+
assert "grant_type=refresh_token" in responses.calls[0].request.body

watson_developer_cloud/assistant_v1.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ class AssistantV1(WatsonService):
3535

3636
default_url = 'https://gateway.watsonplatform.net/assistant/api'
3737

38-
def __init__(self, version, url=default_url, username=None, password=None):
38+
def __init__(self,
39+
version,
40+
url=default_url,
41+
username=None,
42+
password=None,
43+
iam_api_key=None,
44+
iam_access_token=None,
45+
iam_url=None):
3946
"""
4047
Construct a new client for the Assistant service.
4148
@@ -66,6 +73,17 @@ def __init__(self, version, url=default_url, username=None, password=None):
6673
Bluemix, the credentials will be automatically loaded from the
6774
`VCAP_SERVICES` environment variable.
6875
76+
:param str iam_api_key: An API key that can be used to request IAM tokens. If
77+
this API key is provided, the SDK will manage the token and handle the
78+
refreshing.
79+
80+
:param str iam_access_token: An IAM access token is fully managed by the application.
81+
Responsibility falls on the application to refresh the token, either before
82+
it expires or reactively upon receiving a 401 from the service as any requests
83+
made with an expired token will fail.
84+
85+
:param str iam_url: An optional URL for the IAM service API. Defaults to
86+
'https://iam.ng.bluemix.net/identity/token'.
6987
"""
7088

7189
WatsonService.__init__(
@@ -74,6 +92,9 @@ def __init__(self, version, url=default_url, username=None, password=None):
7492
url=url,
7593
username=username,
7694
password=password,
95+
iam_api_key=iam_api_key,
96+
iam_access_token=iam_access_token,
97+
iam_url=iam_url,
7798
use_vcap_services=True)
7899
self.version = version
79100

watson_developer_cloud/conversation_v1.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ class ConversationV1(WatsonService):
3535

3636
default_url = 'https://gateway.watsonplatform.net/conversation/api'
3737

38-
def __init__(self, version, url=default_url, username=None, password=None):
38+
def __init__(self,
39+
version,
40+
url=default_url,
41+
username=None,
42+
password=None,
43+
iam_api_key=None,
44+
iam_access_token=None,
45+
iam_url=None):
3946
"""
4047
Construct a new client for the Conversation service.
4148
@@ -66,6 +73,17 @@ def __init__(self, version, url=default_url, username=None, password=None):
6673
Bluemix, the credentials will be automatically loaded from the
6774
`VCAP_SERVICES` environment variable.
6875
76+
:param str iam_api_key: An API key that can be used to request IAM tokens. If
77+
this API key is provided, the SDK will manage the token and handle the
78+
refreshing.
79+
80+
:param str iam_access_token: An IAM access token is fully managed by the application.
81+
Responsibility falls on the application to refresh the token, either before
82+
it expires or reactively upon receiving a 401 from the service as any requests
83+
made with an expired token will fail.
84+
85+
:param str iam_url: An optional URL for the IAM service API. Defaults to
86+
'https://iam.ng.bluemix.net/identity/token'.
6987
"""
7088

7189
WatsonService.__init__(
@@ -74,6 +92,9 @@ def __init__(self, version, url=default_url, username=None, password=None):
7492
url=url,
7593
username=username,
7694
password=password,
95+
iam_api_key=iam_api_key,
96+
iam_access_token=iam_access_token,
97+
iam_url=iam_url,
7798
use_vcap_services=True)
7899
self.version = version
79100

0 commit comments

Comments
 (0)