Skip to content

Commit 2e776c2

Browse files
committed
feat(IAM Authenticator): add support for optional 'scope' property
1 parent d41c9d8 commit 2e776c2

File tree

7 files changed

+130
-6
lines changed

7 files changed

+130
-6
lines changed

ibm_cloud_sdk_core/authenticators/iam_authenticator.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class IAMAuthenticator(Authenticator):
4545
proxies: Dictionary for mapping request protocol to proxy URL. Defaults to None.
4646
proxies.http (optional): The proxy endpoint to use for HTTP requests.
4747
proxies.https (optional): The proxy endpoint to use for HTTPS requests.
48+
scope: The "scope" to use when fetching the bearer token from the IAM token server.
49+
This can be used to obtain an access token with a specific scope.
4850
4951
Attributes:
5052
token_manager (IAMTokenManager): Retrives and manages IAM tokens from the endpoint specified by the url.
@@ -62,11 +64,12 @@ def __init__(self,
6264
client_secret: Optional[str] = None,
6365
disable_ssl_verification: bool = False,
6466
headers: Optional[Dict[str, str]] = None,
65-
proxies: Optional[Dict[str, str]] = None) -> None:
67+
proxies: Optional[Dict[str, str]] = None,
68+
scope: Optional[str] = None) -> None:
6669
self.token_manager = IAMTokenManager(
6770
apikey, url=url, client_id=client_id, client_secret=client_secret,
6871
disable_ssl_verification=disable_ssl_verification,
69-
headers=headers, proxies=proxies)
72+
headers=headers, proxies=proxies, scope=scope)
7073
self.validate()
7174

7275
def validate(self) -> None:
@@ -147,3 +150,12 @@ def set_proxies(self, proxies: Dict[str, str]) -> None:
147150
proxies.https (optional): The proxy endpoint to use for HTTPS requests.
148151
"""
149152
self.token_manager.set_proxies(proxies)
153+
154+
def set_scope(self, value: str) -> None:
155+
"""Sets the "scope" parameter to use when fetching the bearer token from the IAM token server.
156+
This can be used to obtain an access token with a specific scope.
157+
158+
Args:
159+
value: A space seperated string that makes up the scope parameter.
160+
"""
161+
self.token_manager.set_scope(value)

ibm_cloud_sdk_core/get_authenticator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ def __construct_authenticator(config: dict) -> Authenticator:
6161
url=config.get('AUTH_URL'),
6262
client_id=config.get('CLIENT_ID'),
6363
client_secret=config.get('CLIENT_SECRET'),
64-
disable_ssl_verification=config.get('AUTH_DISABLE_SSL'))
64+
disable_ssl_verification=config.get('AUTH_DISABLE_SSL'),
65+
scope=config.get('SCOPE'))
6566
elif auth_type == 'noauth':
6667
authenticator = NoAuthAuthenticator()
6768

ibm_cloud_sdk_core/iam_token_manager.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class IAMTokenManager(JWTTokenManager):
3636
proxies.http (str): The proxy endpoint to use for HTTP requests.
3737
proxies.https (str): The proxy endpoint to use for HTTPS requests.
3838
http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests.
39+
scope (str): The "scope" to use when fetching the bearer token from the IAM token server.
40+
This can be used to obtain an access token with a specific scope.
3941
4042
Args:
4143
apikey: A generated APIKey from ibmcloud.
@@ -54,12 +56,15 @@ class IAMTokenManager(JWTTokenManager):
5456
proxies: Proxies to use for communicating with IAM. Defaults to None.
5557
proxies.http: The proxy endpoint to use for HTTP requests.
5658
proxies.https: The proxy endpoint to use for HTTPS requests.
59+
scope: The "scope" to use when fetching the bearer token from the IAM token server.
60+
This can be used to obtain an access token with a specific scope.
5761
"""
5862
DEFAULT_IAM_URL = 'https://iam.cloud.ibm.com/identity/token'
5963
CONTENT_TYPE = 'application/x-www-form-urlencoded'
6064
REQUEST_TOKEN_GRANT_TYPE = 'urn:ibm:params:oauth:grant-type:apikey'
6165
REQUEST_TOKEN_RESPONSE_TYPE = 'cloud_iam'
6266
TOKEN_NAME = 'access_token'
67+
SCOPE = 'scope'
6368

6469
def __init__(self,
6570
apikey: str,
@@ -69,13 +74,15 @@ def __init__(self,
6974
client_secret: Optional[str] = None,
7075
disable_ssl_verification: bool = False,
7176
headers: Optional[Dict[str, str]] = None,
72-
proxies: Optional[Dict[str, str]] = None) -> None:
77+
proxies: Optional[Dict[str, str]] = None,
78+
scope: Optional[str] = None) -> None:
7379
self.apikey = apikey
7480
self.url = url if url else self.DEFAULT_IAM_URL
7581
self.client_id = client_id
7682
self.client_secret = client_secret
7783
self.headers = headers
7884
self.proxies = proxies
85+
self.scope = scope
7986
super().__init__(
8087
self.url, disable_ssl_verification=disable_ssl_verification, token_name=self.TOKEN_NAME)
8188

@@ -101,6 +108,9 @@ def request_token(self) -> dict:
101108
'response_type': self.REQUEST_TOKEN_RESPONSE_TYPE
102109
}
103110

111+
if self.scope is not None and self.scope:
112+
data[self.SCOPE] = self.scope
113+
104114
auth_tuple = None
105115
# If both the client_id and secret were specified by the user, then use them
106116
if self.client_id and self.client_secret:
@@ -148,3 +158,11 @@ def set_proxies(self, proxies: Dict[str, str]) -> None:
148158
self.proxies = proxies
149159
else:
150160
raise TypeError('proxies must be a dictionary')
161+
162+
def set_scope(self, value: str) -> None:
163+
"""Sets the "scope" parameter to use when fetching the bearer token from the IAM token server.
164+
165+
Args:
166+
value: A space seperated string that makes up the scope parameter.
167+
"""
168+
self.scope = value

resources/ibm-credentials.env

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,13 @@ SERVICE_1_APIKEY=V4HXmoUtMjohnsnow=KotN
99
SERVICE_1_CLIENT_ID=somefake========id
1010
SERVICE_1_CLIENT_SECRET===my-client-secret==
1111
SERVICE_1_AUTH_URL=https://iamhost/iam/api=
12-
SERVICE_1_URL=service1.com/api
12+
SERVICE_1_URL=service1.com/api
13+
14+
# Service2 configured with IAM w/scope
15+
SERVICE_2_AUTH_TYPE=iam
16+
SERVICE_2_APIKEY=V4HXmoUtMjohnsnow=KotN
17+
SERVICE_2_CLIENT_ID=somefake========id
18+
SERVICE_2_CLIENT_SECRET===my-client-secret==
19+
SERVICE_2_AUTH_URL=https://iamhost/iam/api=
20+
SERVICE_2_URL=service1.com/api
21+
SERVICE_2_SCOPE=A B C D

test/test_iam_authenticator.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
def test_iam_authenticator():
12-
authenticator = IAMAuthenticator('my_apikey')
12+
authenticator = IAMAuthenticator(apikey='my_apikey')
1313
assert authenticator is not None
1414
assert authenticator.token_manager.url == 'https://iam.cloud.ibm.com/identity/token'
1515
assert authenticator.token_manager.client_id is None
@@ -18,11 +18,15 @@ def test_iam_authenticator():
1818
assert authenticator.token_manager.headers is None
1919
assert authenticator.token_manager.proxies is None
2020
assert authenticator.token_manager.apikey == 'my_apikey'
21+
assert authenticator.token_manager.scope is None
2122

2223
authenticator.set_client_id_and_secret('tom', 'jerry')
2324
assert authenticator.token_manager.client_id == 'tom'
2425
assert authenticator.token_manager.client_secret == 'jerry'
2526

27+
authenticator.set_scope('scope1 scope2 scope3')
28+
assert authenticator.token_manager.scope == 'scope1 scope2 scope3'
29+
2630
with pytest.raises(TypeError) as err:
2731
authenticator.set_headers('dummy')
2832
assert str(err.value) == 'headers must be a dictionary'
@@ -37,6 +41,11 @@ def test_iam_authenticator():
3741
authenticator.set_proxies({'dummy': 'proxies'})
3842
assert authenticator.token_manager.proxies == {'dummy': 'proxies'}
3943

44+
def test_iam_authenticator_with_scope():
45+
authenticator = IAMAuthenticator(apikey='my_apikey', scope='scope1 scope2')
46+
assert authenticator is not None
47+
assert authenticator.token_manager.scope == 'scope1 scope2'
48+
4049

4150
def test_iam_authenticator_validate_failed():
4251
with pytest.raises(ValueError) as err:

test/test_iam_token_manager.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,29 @@ def test_request_token_auth_in_ctor():
6767
assert responses.calls[0].request.url == iam_url
6868
assert responses.calls[0].request.headers['Authorization'] != default_auth_header
6969
assert responses.calls[0].response.text == response
70+
assert 'scope' not in responses.calls[0].response.request.body
71+
72+
@responses.activate
73+
def test_request_token_auth_in_ctor_with_scope():
74+
iam_url = "https://iam.cloud.ibm.com/identity/token"
75+
response = """{
76+
"access_token": "oAeisG8yqPY7sFR_x66Z15",
77+
"token_type": "Bearer",
78+
"expires_in": 3600,
79+
"expiration": 1524167011,
80+
"refresh_token": "jy4gl91BQ"
81+
}"""
82+
default_auth_header = 'Basic Yng6Yng='
83+
responses.add(responses.POST, url=iam_url, body=response, status=200)
84+
85+
token_manager = IAMTokenManager("apikey", url=iam_url, client_id='foo', client_secret='bar', scope='john snow')
86+
token_manager.request_token()
87+
88+
assert len(responses.calls) == 1
89+
assert responses.calls[0].request.url == iam_url
90+
assert responses.calls[0].request.headers['Authorization'] != default_auth_header
91+
assert responses.calls[0].response.text == response
92+
assert 'scope=john+snow' in responses.calls[0].response.request.body
7093

7194
@responses.activate
7295
def test_request_token_unsuccessful():
@@ -119,6 +142,7 @@ def test_request_token_auth_in_ctor_client_id_only():
119142
assert responses.calls[0].request.url == iam_url
120143
assert responses.calls[0].request.headers.get('Authorization') is None
121144
assert responses.calls[0].response.text == response
145+
assert 'scope' not in responses.calls[0].response.request.body
122146

123147
@responses.activate
124148
def test_request_token_auth_in_ctor_secret_only():
@@ -139,6 +163,7 @@ def test_request_token_auth_in_ctor_secret_only():
139163
assert responses.calls[0].request.url == iam_url
140164
assert responses.calls[0].request.headers.get('Authorization') is None
141165
assert responses.calls[0].response.text == response
166+
assert 'scope' not in responses.calls[0].response.request.body
142167

143168
@responses.activate
144169
def test_request_token_auth_in_setter():
@@ -161,6 +186,7 @@ def test_request_token_auth_in_setter():
161186
assert responses.calls[0].request.url == iam_url
162187
assert responses.calls[0].request.headers['Authorization'] != default_auth_header
163188
assert responses.calls[0].response.text == response
189+
assert 'scope' not in responses.calls[0].response.request.body
164190

165191
@responses.activate
166192
def test_request_token_auth_in_setter_client_id_only():
@@ -182,6 +208,7 @@ def test_request_token_auth_in_setter_client_id_only():
182208
assert responses.calls[0].request.url == iam_url
183209
assert responses.calls[0].request.headers.get('Authorization') is None
184210
assert responses.calls[0].response.text == response
211+
assert 'scope' not in responses.calls[0].response.request.body
185212

186213
@responses.activate
187214
def test_request_token_auth_in_setter_secret_only():
@@ -204,3 +231,28 @@ def test_request_token_auth_in_setter_secret_only():
204231
assert responses.calls[0].request.url == iam_url
205232
assert responses.calls[0].request.headers.get('Authorization') is None
206233
assert responses.calls[0].response.text == response
234+
assert 'scope' not in responses.calls[0].response.request.body
235+
236+
@responses.activate
237+
def test_request_token_auth_in_setter_scope():
238+
iam_url = "https://iam.cloud.ibm.com/identity/token"
239+
response = """{
240+
"access_token": "oAeisG8yqPY7sFR_x66Z15",
241+
"token_type": "Bearer",
242+
"expires_in": 3600,
243+
"expiration": 1524167011,
244+
"refresh_token": "jy4gl91BQ"
245+
}"""
246+
responses.add(responses.POST, url=iam_url, body=response, status=200)
247+
248+
token_manager = IAMTokenManager("iam_apikey")
249+
token_manager.set_client_id_and_secret(None, 'bar')
250+
token_manager.set_headers({'user':'header'})
251+
token_manager.set_scope('john snow')
252+
token_manager.request_token()
253+
254+
assert len(responses.calls) == 1
255+
assert responses.calls[0].request.url == iam_url
256+
assert responses.calls[0].request.headers.get('Authorization') is None
257+
assert responses.calls[0].response.text == response
258+
assert 'scope=john+snow' in responses.calls[0].response.request.body

test/test_utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,20 @@ def test_get_authenticator_from_credential_file():
145145
assert authenticator.token_manager.client_id == 'somefake========id'
146146
assert authenticator.token_manager.client_secret == '==my-client-secret=='
147147
assert authenticator.token_manager.url == 'https://iamhost/iam/api='
148+
assert authenticator.token_manager.scope is None
149+
del os.environ['IBM_CREDENTIALS_FILE']
150+
151+
def test_get_authenticator_from_credential_file_scope():
152+
file_path = os.path.join(
153+
os.path.dirname(__file__), '../resources/ibm-credentials.env')
154+
os.environ['IBM_CREDENTIALS_FILE'] = file_path
155+
authenticator = get_authenticator_from_environment('service_2')
156+
assert authenticator is not None
157+
assert authenticator.token_manager.apikey == 'V4HXmoUtMjohnsnow=KotN'
158+
assert authenticator.token_manager.client_id == 'somefake========id'
159+
assert authenticator.token_manager.client_secret == '==my-client-secret=='
160+
assert authenticator.token_manager.url == 'https://iamhost/iam/api='
161+
assert authenticator.token_manager.scope == 'A B C D'
148162
del os.environ['IBM_CREDENTIALS_FILE']
149163

150164
def test_get_authenticator_from_env_variables():
@@ -160,6 +174,15 @@ def test_get_authenticator_from_env_variables():
160174
assert authenticator.token_manager.apikey == 'V4HXmoUtMjohnsnow=KotN'
161175
del os.environ['SERVICE_1_APIKEY']
162176

177+
os.environ['SERVICE_2_APIKEY'] = 'johnsnow'
178+
os.environ['SERVICE_2_SCOPE'] = 'A B C D'
179+
authenticator = get_authenticator_from_environment('service_2')
180+
assert authenticator is not None
181+
assert authenticator.token_manager.apikey == 'johnsnow'
182+
assert authenticator.token_manager.scope == 'A B C D'
183+
del os.environ['SERVICE_2_APIKEY']
184+
del os.environ['SERVICE_2_SCOPE']
185+
163186
def test_vcap_credentials():
164187
vcap_services = '{"test":[{"credentials":{ \
165188
"url":"https://gateway.watsonplatform.net/compare-comply/api",\

0 commit comments

Comments
 (0)