Skip to content

Commit ffb9f2e

Browse files
committed
feat(icp4d): Add support for icp4d
1 parent 8bc1dd2 commit ffb9f2e

11 files changed

+483
-262
lines changed

ibm_cloud_sdk_core/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@
1616
from .base_service import BaseService
1717
from .detailed_response import DetailedResponse
1818
from .iam_token_manager import IAMTokenManager
19+
from .jwt_token_manager import JWTTokenManager
20+
from .icp_token_manager import ICPTokenManager
1921
from .api_exception import ApiException
2022
from .utils import datetime_to_string, string_to_datetime

ibm_cloud_sdk_core/base_service.py

Lines changed: 75 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .version import __version__
2525
from .utils import has_bad_first_or_last_char, remove_null_values, cleanup_values
2626
from .iam_token_manager import IAMTokenManager
27+
from .icp_token_manager import ICPTokenManager
2728
from .detailed_response import DetailedResponse
2829
from .api_exception import ApiException
2930

@@ -55,9 +56,9 @@ class BaseService(object):
5556
SDK_NAME = 'ibm-python-sdk-core'
5657

5758
def __init__(self, vcap_services_name, url, username=None, password=None,
58-
use_vcap_services=True, api_key=None,
59-
iam_apikey=None, iam_access_token=None, iam_url=None, iam_client_id=None, iam_client_secret=None,
60-
display_name=None):
59+
use_vcap_services=True, api_key=None, iam_apikey=None, iam_url=None,
60+
iam_access_token=None, iam_client_id=None, iam_client_secret=None,
61+
display_name=None, icp_access_token=None, authentication_type=None):
6162
"""
6263
It loads credentials with the following preference:
6364
1) Credentials explicitly set in the request
@@ -66,41 +67,50 @@ def __init__(self, vcap_services_name, url, username=None, password=None,
6667
"""
6768
self.url = url
6869
self.http_config = {}
70+
self.authentication_type = authentication_type.lower() if authentication_type else None
6971
self.jar = None
70-
self.api_key = None
71-
self.username = None
72-
self.password = None
73-
self.default_headers = None
74-
self.iam_apikey = None
75-
self.iam_access_token = None
76-
self.iam_url = None
77-
self.iam_client_id = None
78-
self.iam_client_secret = None
72+
self.api_key = api_key
73+
self.username = username
74+
self.password = password
75+
self.iam_apikey = iam_apikey
76+
self.iam_access_token = iam_access_token
77+
self.iam_url = iam_url
78+
self.iam_client_id = iam_client_id
79+
self.iam_client_secret = iam_client_secret
80+
self.icp_access_token = icp_access_token
7981
self.token_manager = None
82+
self.default_headers = None
8083
self.verify = None # Indicates whether to ignore verifying the SSL certification
8184

82-
if has_bad_first_or_last_char(self.url):
83-
raise ValueError('The URL shouldn\'t start or end with curly brackets or quotes. '
84-
'Be sure to remove any {} and \" characters surrounding your URL')
85+
self._check_credentials()
8586

8687
self.set_user_agent_header(self.build_user_agent())
8788

8889
# 1. Credentials are passed in constructor
89-
if api_key is not None:
90-
if api_key.startswith(self.ICP_PREFIX):
91-
self.set_username_and_password(self.APIKEY, api_key)
92-
else:
93-
self.set_token_manager(api_key, iam_access_token, iam_url, iam_client_id, iam_client_secret)
94-
elif username is not None and password is not None:
95-
if username is self.APIKEY and not password.startswith(self.ICP_PREFIX):
96-
self.set_token_manager(password, iam_access_token, iam_url, iam_client_id, iam_client_secret)
97-
else:
98-
self.set_username_and_password(username, password)
99-
elif iam_access_token is not None or iam_apikey is not None:
100-
if iam_apikey and iam_apikey.startswith(self.ICP_PREFIX):
101-
self.set_username_and_password(self.APIKEY, iam_apikey)
102-
else:
103-
self.set_token_manager(iam_apikey, iam_access_token, iam_url, iam_client_id, iam_client_secret)
90+
if self.authentication_type == 'iam' or self._has_iam_credentials(self.iam_apikey, self.iam_access_token) or self._has_iam_credentials(self.api_key, self.iam_access_token):
91+
self.token_manager = IAMTokenManager(self.iam_apikey or self.api_key or self.password,
92+
self.iam_access_token,
93+
self.iam_url,
94+
self.iam_client_id,
95+
self.iam_client_secret)
96+
self.iam_apikey = self.iam_apikey or self.api_key or self.password
97+
elif self._uses_basic_for_iam(self.username, self.password):
98+
self.token_manager = IAMTokenManager(self.password,
99+
self.iam_access_token,
100+
self.iam_url,
101+
self.iam_client_id,
102+
self.iam_client_secret)
103+
self.iam_apikey = self.password
104+
self.username = None
105+
self.password = None
106+
elif self._is_for_icp4d(self.authentication_type, self.icp_access_token):
107+
self.token_manager = ICPTokenManager(self.url,
108+
self.username,
109+
self.password,
110+
self.icp_access_token)
111+
elif self._is_for_icp(self.api_key) or self._is_for_icp(self.iam_apikey):
112+
self.username = self.APIKEY
113+
self.password = self.api_key or self.iam_apikey
104114

105115
# 2. Credentials from credential file
106116
if display_name and not self.username and not self.token_manager:
@@ -183,6 +193,41 @@ def _load_from_vcap_services(self, service_name):
183193
else:
184194
return None
185195

196+
def _is_for_icp(self, credential=None):
197+
return credential and credential.startswith(self.ICP_PREFIX)
198+
199+
def _is_for_icp4d(self, authentication_type, icp_access_token=None):
200+
return authentication_type == 'icp4d' or icp_access_token
201+
202+
def _has_basic_credentials(self, username, password):
203+
return username and password and not self._uses_basic_for_iam(username, password)
204+
205+
def _has_iam_credentials(self, iam_apikey, iam_access_token):
206+
return (iam_apikey or iam_access_token) and not self._is_for_icp(iam_apikey)
207+
208+
def _uses_basic_for_iam(self, username, password):
209+
"""
210+
Returns true if the user provides basic auth creds with the intention
211+
of using IAM auth
212+
"""
213+
return username and password and username == self.APIKEY and not self._is_for_icp(password)
214+
215+
def _has_bad_first_or_last_char(self, str):
216+
return str is not None and (str.startswith('{') or str.startswith('"') or str.endswith('}') or str.endswith('"'))
217+
218+
def _check_credentials(self):
219+
credentials_to_check = {
220+
'URL': self.url,
221+
'username': self.username,
222+
'password': self.password,
223+
'credentials': self.iam_apikey
224+
}
225+
226+
for key in credentials_to_check:
227+
if self._has_bad_first_or_last_char(credentials_to_check.get(key)):
228+
raise ValueError('The ' + key + ' shouldn\'t start or end with curly brackets or quotes. '
229+
'Be sure to remove any {} and \" characters surrounding your ' + key)
230+
186231
def disable_SSL_verification(self):
187232
self.verify = False
188233

ibm_cloud_sdk_core/iam_token_manager.py

Lines changed: 15 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -14,69 +14,23 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
import requests
18-
import time
19-
from .api_exception import ApiException
17+
from .jwt_token_manager import JWTTokenManager
2018

21-
class IAMTokenManager(object):
19+
class IAMTokenManager(JWTTokenManager):
2220
DEFAULT_IAM_URL = 'https://iam.cloud.ibm.com/identity/token'
2321
CONTENT_TYPE = 'application/x-www-form-urlencoded'
2422
REQUEST_TOKEN_GRANT_TYPE = 'urn:ibm:params:oauth:grant-type:apikey'
2523
REQUEST_TOKEN_RESPONSE_TYPE = 'cloud_iam'
26-
REFRESH_TOKEN_GRANT_TYPE = 'refresh_token'
2724

2825
def __init__(self, iam_apikey=None, iam_access_token=None, iam_url=None,
2926
iam_client_id=None, iam_client_secret=None):
3027
self.iam_apikey = iam_apikey
31-
self.user_access_token = iam_access_token
3228
self.iam_url = iam_url if iam_url else self.DEFAULT_IAM_URL
3329
self.iam_client_id = iam_client_id
3430
self.iam_client_secret = iam_client_secret
35-
self.token_info = {
36-
'access_token': None,
37-
'refresh_token': None,
38-
'token_type': None,
39-
'expires_in': None,
40-
'expiration': None,
41-
}
42-
43-
def request(self, method, url, headers=None, params=None, data=None, **kwargs):
44-
auth_tuple = ('bx', 'bx')
45-
if self.iam_client_id and self.iam_client_secret:
46-
auth_tuple = (self.iam_client_id, self.iam_client_secret)
47-
response = requests.request(method=method, url=url,
48-
headers=headers, params=params,
49-
data=data, auth=auth_tuple, **kwargs)
50-
if 200 <= response.status_code <= 299:
51-
return response.json()
52-
else:
53-
raise ApiException(response.status_code, http_response=response)
31+
super(IAMTokenManager, self).__init__(self.iam_url, iam_access_token)
5432

55-
def get_token(self):
56-
"""
57-
The source of the token is determined by the following logic:
58-
1. If user provides their own managed access token, assume it is valid and send it
59-
2. If this class is managing tokens and does not yet have one, make a request for one
60-
3. If this class is managing tokens and the token has expired refresh it. In case the refresh token is expired, get a new one
61-
If this class is managing tokens and has a valid token stored, send it
62-
"""
63-
if self.user_access_token:
64-
return self.user_access_token
65-
elif not self.token_info.get('access_token'):
66-
token_info = self._request_token()
67-
self._save_token_info(token_info)
68-
return self.token_info.get('access_token')
69-
elif self._is_token_expired():
70-
if self._is_refresh_token_expired():
71-
token_info = self._request_token()
72-
else:
73-
token_info = self._refresh_token()
74-
self._save_token_info(token_info)
75-
return self.token_info.get('access_token')
76-
else:
77-
return self.token_info.get('access_token')
78-
79-
def _request_token(self):
33+
def request_token(self):
8034
"""
8135
Request an IAM token using an API key
8236
"""
@@ -89,39 +43,22 @@ def _request_token(self):
8943
'apikey': self.iam_apikey,
9044
'response_type': self.REQUEST_TOKEN_RESPONSE_TYPE
9145
}
92-
response = self.request(
93-
method='POST',
94-
url=self.iam_url,
95-
headers=headers,
96-
data=data)
97-
return response
9846

99-
def _refresh_token(self):
100-
"""
101-
Refresh an IAM token using a refresh token
102-
"""
103-
headers = {
104-
'Content-type': self.CONTENT_TYPE,
105-
'Accept': 'application/json'
106-
}
107-
data = {
108-
'grant_type': self.REFRESH_TOKEN_GRANT_TYPE,
109-
'refresh_token': self.token_info.get('refresh_token')
110-
}
111-
response = self.request(
47+
# Use bx:bx as default auth header creds
48+
auth_tuple = ('bx', 'bx')
49+
50+
# If both the clientId and secret were specified by the user, then use them
51+
if self.iam_client_id and self.iam_client_secret:
52+
auth_tuple = (self.iam_client_id, self.iam_client_secret)
53+
54+
response = self._request(
11255
method='POST',
113-
url=self.iam_url,
56+
url=self.url,
11457
headers=headers,
115-
data=data)
58+
data=data,
59+
auth_tuple=auth_tuple)
11660
return response
11761

118-
def set_access_token(self, iam_access_token):
119-
"""
120-
Set a self-managed IAM access token.
121-
The access token should be valid and not yet expired.
122-
"""
123-
self.user_access_token = iam_access_token
124-
12562
def set_iam_apikey(self, iam_apikey):
12663
"""
12764
Set the IAM api key
@@ -145,40 +82,3 @@ def set_iam_authorization_info(self, iam_client_id, iam_client_secret):
14582
"""
14683
self.iam_client_id = iam_client_id
14784
self.iam_client_secret = iam_client_secret
148-
149-
def _is_token_expired(self):
150-
"""
151-
Check if currently stored token is expired.
152-
153-
Using a buffer to prevent the edge case of the
154-
oken expiring before the request could be made.
155-
156-
The buffer will be a fraction of the total TTL. Using 80%.
157-
"""
158-
fraction_of_ttl = 0.8
159-
time_to_live = self.token_info.get('expires_in')
160-
expire_time = self.token_info.get('expiration')
161-
refresh_time = expire_time - (time_to_live * (1.0 - fraction_of_ttl))
162-
current_time = int(time.time())
163-
return refresh_time < current_time
164-
165-
def _is_refresh_token_expired(self):
166-
"""
167-
Used as a fail-safe to prevent the condition of a refresh token expiring,
168-
which could happen after around 30 days. This function will return true
169-
if it has been at least 7 days and 1 hour since the last token was set
170-
"""
171-
if self.token_info.get('expiration') is None:
172-
return True
173-
174-
seven_days = 7 * 24 * 3600
175-
current_time = int(time.time())
176-
new_token_time = self.token_info.get('expiration') + seven_days
177-
return new_token_time < current_time
178-
179-
def _save_token_info(self, token_info):
180-
"""
181-
Save the response from the IAM service request to the object's state.
182-
"""
183-
self.token_info = token_info
184-
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# coding: utf-8
2+
3+
# Copyright 2019 IBM All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
from .jwt_token_manager import JWTTokenManager
18+
19+
class ICPTokenManager(JWTTokenManager):
20+
def __init__(self, url, username=None, password=None, access_token=None):
21+
url = url + '/v1/preauth/validateAuth'
22+
self.username = username
23+
self.password = password
24+
super(ICPTokenManager, self).__init__(url, access_token)
25+
26+
def request_token(self):
27+
auth_tuple = (self.username, self.password)
28+
29+
response = self._request(
30+
method='GET',
31+
url=self.url,
32+
auth_tuple=auth_tuple)
33+
return response

0 commit comments

Comments
 (0)