Skip to content

Commit 37014b5

Browse files
authored
feat: send user-agent header with auth token requests (#191)
This commit updates our various request-based authenticators so that the User-Agent header is included with each outbound token request. The value of the User-Agent header will be of the form "ibm-python-sdk-core/<authenticator-type>-<core-version> <os-info>". Signed-off-by: Phil Adams <[email protected]>
1 parent 6a03380 commit 37014b5

16 files changed

+150
-28
lines changed

ibm_cloud_sdk_core/api_exception.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def code(self):
5454
"""The old `code` property with a deprecation warning."""
5555

5656
warnings.warn(
57-
'Using the `code` attribute on the `ApiException` is deprecated and'
57+
'Using the `code` attribute on the `ApiException` is deprecated and '
5858
'will be removed in the future. Use `status_code` instead.',
5959
DeprecationWarning,
6060
)

ibm_cloud_sdk_core/base_service.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# coding: utf-8
22

3-
# Copyright 2019 IBM All Rights Reserved.
3+
# Copyright 2019, 2024 IBM All Rights Reserved.
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@
1818
import io
1919
import json as json_import
2020
import logging
21-
import platform
2221
from http.cookiejar import CookieJar
2322
from os.path import basename
2423
from typing import Dict, List, Optional, Tuple, Union
@@ -42,7 +41,7 @@
4241
SSLHTTPAdapter,
4342
GzipStream,
4443
)
45-
from .version import __version__
44+
from .private_helpers import _build_user_agent
4645

4746
# Uncomment this to enable http debugging
4847
# import http.client as http_client
@@ -82,7 +81,6 @@ class BaseService:
8281
ValueError: If Authenticator is not provided or invalid type.
8382
"""
8483

85-
SDK_NAME = 'ibm-python-sdk-core'
8684
ERROR_MSG_DISABLE_SSL = (
8785
'The connection failed because the SSL certificate is not valid. To use a self-signed '
8886
'certificate, disable verification of the server\'s SSL certificate by invoking the '
@@ -106,7 +104,7 @@ def __init__(
106104
self.disable_ssl_verification = disable_ssl_verification
107105
self.default_headers = None
108106
self.enable_gzip_compression = enable_gzip_compression
109-
self._set_user_agent_header(self._build_user_agent())
107+
self._set_user_agent_header(_build_user_agent())
110108
self.retry_config = None
111109
self.http_adapter = SSLHTTPAdapter(_disable_ssl_verification=self.disable_ssl_verification)
112110
if not self.authenticator:
@@ -151,15 +149,6 @@ def disable_retries(self):
151149
self.http_client.mount('http://', self.http_adapter)
152150
self.http_client.mount('https://', self.http_adapter)
153151

154-
@staticmethod
155-
def _get_system_info() -> str:
156-
return '{0} {1} {2}'.format(
157-
platform.system(), platform.release(), platform.python_version() # OS # OS version # Python version
158-
)
159-
160-
def _build_user_agent(self) -> str:
161-
return '{0}-{1} {2}'.format(self.SDK_NAME, __version__, self._get_system_info())
162-
163152
def configure_service(self, service_name: str) -> None:
164153
"""Look for external configuration of a service. Set service properties.
165154

ibm_cloud_sdk_core/private_helpers.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# coding: utf-8
2+
3+
# Copyright 2024 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+
# from ibm_cloud_sdk_core.authenticators import Authenticator
17+
18+
import platform
19+
from .version import __version__
20+
21+
SDK_NAME = 'ibm-python-sdk-core'
22+
23+
24+
def _get_system_info() -> str:
25+
return 'os.name={0} os.version={1} python.version={2}'.format(
26+
platform.system(), platform.release(), platform.python_version()
27+
)
28+
29+
30+
def _build_user_agent(component: str = None) -> str:
31+
sub_component = ""
32+
if component is not None:
33+
sub_component = '/{0}'.format(component)
34+
return '{0}{1}-{2} {3}'.format(SDK_NAME, sub_component, __version__, _get_system_info())

ibm_cloud_sdk_core/token_managers/container_token_manager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# coding: utf-8
22

3-
# Copyright 2021 IBM All Rights Reserved.
3+
# Copyright 2021, 2024 IBM All Rights Reserved.
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818
from typing import Dict, Optional
1919

2020
from .iam_request_based_token_manager import IAMRequestBasedTokenManager
21+
from ..private_helpers import _build_user_agent
2122

2223

2324
logger = logging.getLogger(__name__)
@@ -111,6 +112,7 @@ def __init__(
111112
self.iam_profile_id = iam_profile_id
112113

113114
self.request_payload['grant_type'] = 'urn:ibm:params:oauth:grant-type:cr-token'
115+
self._set_user_agent(_build_user_agent('container-authenticator'))
114116

115117
def retrieve_cr_token(self) -> str:
116118
"""Retrieves the CR token for the current compute resource by reading it from the local file system.

ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# coding: utf-8
22

3-
# Copyright 2019 IBM All Rights Reserved.
3+
# Copyright 2019, 2024 IBM All Rights Reserved.
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
import json
1818
from typing import Dict, Optional
1919

20+
from ..private_helpers import _build_user_agent
2021
from .jwt_token_manager import JWTTokenManager
2122

2223

@@ -76,12 +77,21 @@ def __init__(
7677
self.headers['Content-Type'] = 'application/json'
7778
self.proxies = proxies
7879
super().__init__(url, disable_ssl_verification=disable_ssl_verification, token_name=self.TOKEN_NAME)
80+
self._set_user_agent(_build_user_agent('cp4d-authenticator'))
7981

8082
def request_token(self) -> dict:
8183
"""Makes a request for a token."""
84+
required_headers = {
85+
'User-Agent': self.user_agent,
86+
}
87+
request_headers = {}
88+
if self.headers is not None and isinstance(self.headers, dict):
89+
request_headers.update(self.headers)
90+
request_headers.update(required_headers)
91+
8292
response = self._request(
8393
method='POST',
84-
headers=self.headers,
94+
headers=request_headers,
8595
url=self.url,
8696
data=json.dumps({"username": self.username, "password": self.password, "api_key": self.apikey}),
8797
proxies=self.proxies,

ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,15 @@ def request_token(self) -> dict:
9898
Returns:
9999
A dictionary containing the bearer token to be subsequently used service requests.
100100
"""
101-
headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}
101+
required_headers = {
102+
'Content-Type': 'application/x-www-form-urlencoded',
103+
'Accept': 'application/json',
104+
'User-Agent': self._get_user_agent(),
105+
}
106+
request_headers = {}
102107
if self.headers is not None and isinstance(self.headers, dict):
103-
headers.update(self.headers)
108+
request_headers.update(self.headers)
109+
request_headers.update(required_headers)
104110

105111
data = dict(self.request_payload)
106112

@@ -115,7 +121,7 @@ def request_token(self) -> dict:
115121
response = self._request(
116122
method='POST',
117123
url=(self.url + self.OPERATION_PATH) if self.url else self.url,
118-
headers=headers,
124+
headers=request_headers,
119125
data=data,
120126
auth_tuple=auth_tuple,
121127
proxies=self.proxies,

ibm_cloud_sdk_core/token_managers/iam_token_manager.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# coding: utf-8
22

3-
# Copyright 2019 IBM All Rights Reserved.
3+
# Copyright 2019, 2024 IBM All Rights Reserved.
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
from typing import Dict, Optional
1818

1919
from .iam_request_based_token_manager import IAMRequestBasedTokenManager
20+
from ..private_helpers import _build_user_agent
2021

2122

2223
class IAMTokenManager(IAMRequestBasedTokenManager):
@@ -88,3 +89,5 @@ def __init__(
8889
self.request_payload['grant_type'] = 'urn:ibm:params:oauth:grant-type:apikey'
8990
self.request_payload['apikey'] = self.apikey
9091
self.request_payload['response_type'] = 'cloud_iam'
92+
93+
self._set_user_agent(_build_user_agent('iam-authenticator'))

ibm_cloud_sdk_core/token_managers/mcsp_token_manager.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# coding: utf-8
22

3-
# Copyright 2023 IBM All Rights Reserved.
3+
# Copyright 2023, 2024 IBM All Rights Reserved.
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
import json
1818
from typing import Dict, Optional
1919

20+
from ..private_helpers import _build_user_agent
2021
from .jwt_token_manager import JWTTokenManager
2122

2223

@@ -55,12 +56,21 @@ def __init__(
5556
self.headers['Accept'] = 'application/json'
5657
self.proxies = proxies
5758
super().__init__(url, disable_ssl_verification=disable_ssl_verification, token_name=self.TOKEN_NAME)
59+
self._set_user_agent(_build_user_agent('mcsp-authenticator'))
5860

5961
def request_token(self) -> dict:
6062
"""Makes a request for a token."""
63+
required_headers = {
64+
'User-Agent': self.user_agent,
65+
}
66+
request_headers = {}
67+
if self.headers is not None and isinstance(self.headers, dict):
68+
request_headers.update(self.headers)
69+
request_headers.update(required_headers)
70+
6171
response = self._request(
6272
method='POST',
63-
headers=self.headers,
73+
headers=request_headers,
6474
url=self.url + self.OPERATION_PATH,
6575
data=json.dumps({"apikey": self.apikey}),
6676
proxies=self.proxies,

ibm_cloud_sdk_core/token_managers/token_manager.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# coding: utf-8
22

3-
# Copyright 2020 IBM All Rights Reserved.
3+
# Copyright 2020, 2024 IBM All Rights Reserved.
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -49,6 +49,7 @@ class TokenManager(ABC):
4949
lock (Lock): Lock variable to serialize access to refresh/request times
5050
http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests.
5151
access_token (str): The latest stored access token
52+
user_agent (str): The User-Agent header value to be included in each outbound token request
5253
"""
5354

5455
def __init__(self, url: str, *, disable_ssl_verification: bool = False):
@@ -60,6 +61,7 @@ def __init__(self, url: str, *, disable_ssl_verification: bool = False):
6061
self.lock = Lock()
6162
self.http_config = {}
6263
self.access_token = None
64+
self.user_agent = None
6365

6466
def get_token(self) -> str:
6567
"""Get a token to be used for authentication.
@@ -95,6 +97,12 @@ def set_disable_ssl_verification(self, status: bool = False) -> None:
9597
else:
9698
raise TypeError('status must be a bool')
9799

100+
def _set_user_agent(self, user_agent: str = None) -> None:
101+
self.user_agent = user_agent
102+
103+
def _get_user_agent(self) -> str:
104+
return self.user_agent
105+
98106
def paced_request_token(self) -> None:
99107
"""
100108
Paces requests to request_token.

ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import logging
1919
from typing import Optional
2020

21+
from ..private_helpers import _build_user_agent
2122
from .jwt_token_manager import JWTTokenManager
2223

2324

@@ -64,6 +65,7 @@ def __init__(
6465
url = self.DEFAULT_IMS_ENDPOINT
6566

6667
super().__init__(url, token_name=self.TOKEN_NAME)
68+
self._set_user_agent(_build_user_agent('vpc-instance-authenticator'))
6769

6870
self.iam_profile_crn = iam_profile_crn
6971
self.iam_profile_id = iam_profile_id
@@ -92,6 +94,7 @@ def request_token(self) -> dict:
9294
'Content-Type': 'application/json',
9395
'Accept': 'application/json',
9496
'Authorization': 'Bearer ' + instance_identity_token,
97+
'User-Agent': self._get_user_agent(),
9598
}
9699

97100
logger.debug('Invoking VPC \'create_iam_token\' operation: %s', url)
@@ -138,6 +141,7 @@ def retrieve_instance_identity_token(self) -> str:
138141
'Content-type': 'application/json',
139142
'Accept': 'application/json',
140143
'Metadata-Flavor': 'ibm',
144+
'User-Agent': self._get_user_agent(),
141145
}
142146

143147
request_body = {'expires_in': 300}

test/test_base_service.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
1-
# coding=utf-8
1+
# coding: utf-8
2+
3+
# Copyright 2019, 2024 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+
217
# pylint: disable=missing-docstring,protected-access,too-few-public-methods,too-many-lines
18+
319
import gzip
420
import json
521
import os
@@ -802,7 +818,7 @@ def test_user_agent_header():
802818
service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator())
803819
user_agent_header = service.user_agent_header
804820
assert user_agent_header is not None
805-
assert user_agent_header['User-Agent'] is not None
821+
assert user_agent_header['User-Agent'].startswith('ibm-python-sdk-core-')
806822

807823
responses.add(responses.GET, 'https://gateway.watsonplatform.net/test/api', status=200, body='some text')
808824
prepped = service.prepare_request('GET', url='', headers={'user-agent': 'my_user_agent'})

test/test_container_token_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ def test_request_token_auth_default():
111111
assert len(responses.calls) == 1
112112
assert responses.calls[0].request.url == iam_url
113113
assert responses.calls[0].request.headers.get('Authorization') is None
114+
assert (
115+
responses.calls[0].request.headers.get('User-Agent').startswith('ibm-python-sdk-core/container-authenticator')
116+
)
114117
assert json.loads(responses.calls[0].response.text)['access_token'] == TEST_ACCESS_TOKEN_1
115118

116119

test/test_cp4d_token_manager.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
# coding: utf-8
2+
3+
# Copyright 2019, 2024 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+
117
# pylint: disable=missing-docstring
218
import json
319
import time
@@ -38,6 +54,7 @@ def test_request_token():
3854

3955
assert len(responses.calls) == 1
4056
assert responses.calls[0].request.url == url + '/v1/authorize'
57+
assert responses.calls[0].request.headers.get('User-Agent').startswith('ibm-python-sdk-core/cp4d-authenticator')
4158
assert token == access_token
4259

4360
token_manager = CP4DTokenManager("username", "password", url + '/v1/authorize')

0 commit comments

Comments
 (0)