ibm-cloud-sdk-core 3.16.0__py3-none-any.whl → 3.21.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. ibm_cloud_sdk_core/__init__.py +1 -0
  2. ibm_cloud_sdk_core/api_exception.py +18 -4
  3. ibm_cloud_sdk_core/authenticators/__init__.py +1 -0
  4. ibm_cloud_sdk_core/authenticators/authenticator.py +2 -1
  5. ibm_cloud_sdk_core/authenticators/basic_authenticator.py +12 -7
  6. ibm_cloud_sdk_core/authenticators/bearer_token_authenticator.py +7 -2
  7. ibm_cloud_sdk_core/authenticators/container_authenticator.py +25 -16
  8. ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py +38 -23
  9. ibm_cloud_sdk_core/authenticators/iam_authenticator.py +22 -13
  10. ibm_cloud_sdk_core/authenticators/iam_request_based_authenticator.py +10 -8
  11. ibm_cloud_sdk_core/authenticators/mcsp_authenticator.py +134 -0
  12. ibm_cloud_sdk_core/authenticators/vpc_instance_authenticator.py +12 -11
  13. ibm_cloud_sdk_core/base_service.py +137 -103
  14. ibm_cloud_sdk_core/detailed_response.py +21 -15
  15. ibm_cloud_sdk_core/get_authenticator.py +35 -17
  16. ibm_cloud_sdk_core/http_adapter.py +28 -0
  17. ibm_cloud_sdk_core/logger.py +85 -0
  18. ibm_cloud_sdk_core/private_helpers.py +34 -0
  19. ibm_cloud_sdk_core/token_managers/container_token_manager.py +63 -33
  20. ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py +35 -22
  21. ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py +50 -21
  22. ibm_cloud_sdk_core/token_managers/iam_token_manager.py +24 -13
  23. ibm_cloud_sdk_core/token_managers/jwt_token_manager.py +3 -16
  24. ibm_cloud_sdk_core/token_managers/mcsp_token_manager.py +108 -0
  25. ibm_cloud_sdk_core/token_managers/token_manager.py +20 -23
  26. ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py +37 -18
  27. ibm_cloud_sdk_core/utils.py +127 -48
  28. ibm_cloud_sdk_core/version.py +1 -1
  29. ibm_cloud_sdk_core-3.21.0.dist-info/METADATA +159 -0
  30. ibm_cloud_sdk_core-3.21.0.dist-info/RECORD +35 -0
  31. {ibm_cloud_sdk_core-3.16.0.dist-info → ibm_cloud_sdk_core-3.21.0.dist-info}/WHEEL +1 -1
  32. ibm_cloud_sdk_core-3.21.0.dist-info/top_level.txt +1 -0
  33. ibm_cloud_sdk_core-3.16.0.dist-info/METADATA +0 -112
  34. ibm_cloud_sdk_core-3.16.0.dist-info/RECORD +0 -52
  35. ibm_cloud_sdk_core-3.16.0.dist-info/top_level.txt +0 -3
  36. ibm_cloud_sdk_core-3.16.0.dist-info/zip-safe +0 -1
  37. test/__init__.py +0 -0
  38. test/test_api_exception.py +0 -73
  39. test/test_authenticator.py +0 -21
  40. test/test_base_service.py +0 -933
  41. test/test_basic_authenticator.py +0 -36
  42. test/test_bearer_authenticator.py +0 -28
  43. test/test_container_authenticator.py +0 -105
  44. test/test_container_token_manager.py +0 -283
  45. test/test_cp4d_authenticator.py +0 -171
  46. test/test_cp4d_token_manager.py +0 -56
  47. test/test_detailed_response.py +0 -57
  48. test/test_iam_authenticator.py +0 -157
  49. test/test_iam_token_manager.py +0 -362
  50. test/test_jwt_token_manager.py +0 -109
  51. test/test_no_auth_authenticator.py +0 -15
  52. test/test_token_manager.py +0 -84
  53. test/test_utils.py +0 -634
  54. test/test_vpc_instance_authenticator.py +0 -66
  55. test/test_vpc_instance_token_manager.py +0 -266
  56. test_integration/__init__.py +0 -0
  57. test_integration/test_cp4d_authenticator_integration.py +0 -45
  58. {ibm_cloud_sdk_core-3.16.0.dist-info → ibm_cloud_sdk_core-3.21.0.dist-info}/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- # Copyright 2019 IBM All Rights Reserved.
3
+ # Copyright 2019, 2024 IBM All Rights Reserved.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -14,10 +14,21 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
- from .authenticators import (Authenticator, BasicAuthenticator, BearerTokenAuthenticator, ContainerAuthenticator,
18
- CloudPakForDataAuthenticator, IAMAuthenticator, NoAuthAuthenticator,
19
- VPCInstanceAuthenticator)
17
+ from .authenticators import (
18
+ Authenticator,
19
+ BasicAuthenticator,
20
+ BearerTokenAuthenticator,
21
+ ContainerAuthenticator,
22
+ CloudPakForDataAuthenticator,
23
+ IAMAuthenticator,
24
+ NoAuthAuthenticator,
25
+ VPCInstanceAuthenticator,
26
+ MCSPAuthenticator,
27
+ )
20
28
  from .utils import read_external_sources
29
+ from .logger import get_logger
30
+
31
+ logger = get_logger()
21
32
 
22
33
 
23
34
  def get_authenticator_from_environment(service_name: str) -> Authenticator:
@@ -34,10 +45,13 @@ def get_authenticator_from_environment(service_name: str) -> Authenticator:
34
45
  Returns:
35
46
  The authenticator found from service information.
36
47
  """
48
+ logger.debug('Get authenticator from environment, key=%s', service_name)
37
49
  authenticator = None
38
50
  config = read_external_sources(service_name)
39
51
  if config:
40
52
  authenticator = __construct_authenticator(config)
53
+ if authenticator is not None:
54
+ logger.debug('Returning authenticator, type=%s', authenticator.authentication_type())
41
55
  return authenticator
42
56
 
43
57
 
@@ -59,12 +73,9 @@ def __construct_authenticator(config: dict) -> Authenticator:
59
73
  authenticator = None
60
74
 
61
75
  if auth_type == Authenticator.AUTHTYPE_BASIC.lower():
62
- authenticator = BasicAuthenticator(
63
- username=config.get('USERNAME'),
64
- password=config.get('PASSWORD'))
76
+ authenticator = BasicAuthenticator(username=config.get('USERNAME'), password=config.get('PASSWORD'))
65
77
  elif auth_type == Authenticator.AUTHTYPE_BEARERTOKEN.lower():
66
- authenticator = BearerTokenAuthenticator(
67
- bearer_token=config.get('BEARER_TOKEN'))
78
+ authenticator = BearerTokenAuthenticator(bearer_token=config.get('BEARER_TOKEN'))
68
79
  elif auth_type == Authenticator.AUTHTYPE_CONTAINER.lower():
69
80
  authenticator = ContainerAuthenticator(
70
81
  cr_token_filename=config.get('CR_TOKEN_FILENAME'),
@@ -73,30 +84,37 @@ def __construct_authenticator(config: dict) -> Authenticator:
73
84
  url=config.get('AUTH_URL'),
74
85
  client_id=config.get('CLIENT_ID'),
75
86
  client_secret=config.get('CLIENT_SECRET'),
76
- disable_ssl_verification=config.get(
77
- 'AUTH_DISABLE_SSL', 'false').lower() == 'true',
78
- scope=config.get('SCOPE'))
87
+ disable_ssl_verification=config.get('AUTH_DISABLE_SSL', 'false').lower() == 'true',
88
+ scope=config.get('SCOPE'),
89
+ )
79
90
  elif auth_type == Authenticator.AUTHTYPE_CP4D.lower():
80
91
  authenticator = CloudPakForDataAuthenticator(
81
92
  username=config.get('USERNAME'),
82
93
  password=config.get('PASSWORD'),
83
94
  url=config.get('AUTH_URL'),
84
95
  apikey=config.get('APIKEY'),
85
- disable_ssl_verification=config.get('AUTH_DISABLE_SSL', 'false').lower() == 'true')
96
+ disable_ssl_verification=config.get('AUTH_DISABLE_SSL', 'false').lower() == 'true',
97
+ )
86
98
  elif auth_type == Authenticator.AUTHTYPE_IAM.lower() and config.get('APIKEY'):
87
99
  authenticator = IAMAuthenticator(
88
100
  apikey=config.get('APIKEY'),
89
101
  url=config.get('AUTH_URL'),
90
102
  client_id=config.get('CLIENT_ID'),
91
103
  client_secret=config.get('CLIENT_SECRET'),
92
- disable_ssl_verification=config.get(
93
- 'AUTH_DISABLE_SSL', 'false').lower() == 'true',
94
- scope=config.get('SCOPE'))
104
+ disable_ssl_verification=config.get('AUTH_DISABLE_SSL', 'false').lower() == 'true',
105
+ scope=config.get('SCOPE'),
106
+ )
95
107
  elif auth_type == Authenticator.AUTHTYPE_VPC.lower():
96
108
  authenticator = VPCInstanceAuthenticator(
97
109
  iam_profile_crn=config.get('IAM_PROFILE_CRN'),
98
110
  iam_profile_id=config.get('IAM_PROFILE_ID'),
99
- url=config.get('AUTH_URL'))
111
+ url=config.get('AUTH_URL'),
112
+ )
113
+ elif auth_type == Authenticator.AUTHTYPE_MCSP.lower():
114
+ authenticator = MCSPAuthenticator(
115
+ apikey=config.get('APIKEY'),
116
+ url=config.get('AUTH_URL'),
117
+ )
100
118
  elif auth_type == Authenticator.AUTHTYPE_NOAUTH.lower():
101
119
  authenticator = NoAuthAuthenticator()
102
120
 
@@ -0,0 +1,28 @@
1
+ import ssl
2
+
3
+ from requests import certs
4
+ from requests.adapters import HTTPAdapter, DEFAULT_POOLBLOCK
5
+ from urllib3.util.ssl_ import create_urllib3_context
6
+
7
+
8
+ class SSLHTTPAdapter(HTTPAdapter):
9
+ """Wraps the original HTTP adapter and adds additional SSL context."""
10
+
11
+ def __init__(self, *args, **kwargs):
12
+ self._disable_ssl_verification = kwargs.pop('_disable_ssl_verification', None)
13
+
14
+ super().__init__(*args, **kwargs)
15
+
16
+ def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs):
17
+ """Create and use custom SSL configuration."""
18
+
19
+ ssl_context = create_urllib3_context()
20
+ # NOTE: https://github.com/psf/requests/pull/6731/files#r1622893724
21
+ ssl_context.load_verify_locations(certs.where())
22
+ ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
23
+
24
+ if self._disable_ssl_verification:
25
+ ssl_context.check_hostname = False
26
+ ssl_context.verify_mode = ssl.CERT_NONE
27
+
28
+ super().init_poolmanager(connections, maxsize, block, ssl_context=ssl_context, **pool_kwargs)
@@ -0,0 +1,85 @@
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
+
17
+ import logging
18
+ import re
19
+
20
+
21
+ # This is the name of the primary logger used by the library.
22
+ LOGGER_NAME = 'ibm-cloud-sdk-core'
23
+ # Keywords that are redacted.
24
+ REDACTED_KEYWORDS = [
25
+ "apikey",
26
+ "api_key",
27
+ "passcode",
28
+ "password",
29
+ "token",
30
+ "aadClientId",
31
+ "aadClientSecret",
32
+ "auth",
33
+ "auth_provider_x509_cert_url",
34
+ "auth_uri",
35
+ "client_email",
36
+ "client_id",
37
+ "client_x509_cert_url",
38
+ "key",
39
+ "project_id",
40
+ "secret",
41
+ "subscriptionId",
42
+ "tenantId",
43
+ "thumbprint",
44
+ "token_uri",
45
+ ]
46
+
47
+
48
+ class LoggingFilter:
49
+ """Functions used to filter messages before they are logged."""
50
+
51
+ redacted_tokens = "|".join(REDACTED_KEYWORDS)
52
+ auth_header_pattern = re.compile(r"(?m)(Authorization|X-Auth\S*): ((.*?)(\r\n.*)|(.*))")
53
+ property_settings_pattern = re.compile(r"(?i)(" + redacted_tokens + r")=[^&]*(&|$)")
54
+ json_field_pattern = re.compile(r'(?i)"([^"]*(' + redacted_tokens + r')[^"_]*)":\s*"[^\,]*"')
55
+
56
+ @classmethod
57
+ def redact_secrets(cls, text: str) -> str:
58
+ """Replaces values of potential secret keywords with a placeholder value.
59
+ Args:
60
+ text (str): the string to check and process
61
+
62
+ Returns:
63
+ str: the safe, redacted string with all secrets masked out
64
+ """
65
+
66
+ placeholder = "[redacted]"
67
+ redacted = cls.auth_header_pattern.sub(r"\1: " + placeholder + r"\4", text)
68
+ redacted = cls.property_settings_pattern.sub(r"\1=" + placeholder + r"\2", redacted)
69
+ redacted = cls.json_field_pattern.sub(r'"\1":"' + placeholder + r'"', redacted)
70
+ return redacted
71
+
72
+ @classmethod
73
+ def filter_message(cls, s: str) -> str:
74
+ """Filters 's' prior to logging it as a debug message"""
75
+ # Redact secrets
76
+ s = LoggingFilter.redact_secrets(s)
77
+
78
+ # Replace CRLF characters with an actual newline to make the message more readable.
79
+ s = s.replace('\\r\\n', '\n')
80
+ return s
81
+
82
+
83
+ def get_logger() -> logging.Logger:
84
+ """Returns the primary logger object instance used by the library."""
85
+ return logging.getLogger(LOGGER_NAME)
@@ -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())
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- # Copyright 2021 IBM All Rights Reserved.
3
+ # Copyright 2021, 2024 IBM All Rights Reserved.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -14,13 +14,13 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
- import logging
18
17
  from typing import Dict, Optional
19
18
 
19
+ from ibm_cloud_sdk_core.logger import get_logger
20
20
  from .iam_request_based_token_manager import IAMRequestBasedTokenManager
21
+ from ..private_helpers import _build_user_agent
21
22
 
22
-
23
- logger = logging.getLogger(__name__)
23
+ logger = get_logger()
24
24
 
25
25
 
26
26
  class ContainerTokenManager(IAMRequestBasedTokenManager):
@@ -79,51 +79,64 @@ class ContainerTokenManager(IAMRequestBasedTokenManager):
79
79
  scope: The "scope" to use when fetching the bearer token from the IAM token server.
80
80
  This can be used to obtain an access token with a specific scope.
81
81
  """
82
- DEFAULT_CR_TOKEN_FILENAME = '/var/run/secrets/tokens/vault-token'
83
-
84
- def __init__(self,
85
- cr_token_filename: Optional[str] = None,
86
- iam_profile_name: Optional[str] = None,
87
- iam_profile_id: Optional[str] = None,
88
- url: Optional[str] = None,
89
- client_id: Optional[str] = None,
90
- client_secret: Optional[str] = None,
91
- disable_ssl_verification: bool = False,
92
- scope: Optional[str] = None,
93
- proxies: Optional[Dict[str, str]] = None,
94
- headers: Optional[Dict[str, str]] = None) -> None:
82
+
83
+ DEFAULT_CR_TOKEN_FILENAME1 = '/var/run/secrets/tokens/vault-token'
84
+ DEFAULT_CR_TOKEN_FILENAME2 = '/var/run/secrets/tokens/sa-token'
85
+
86
+ def __init__(
87
+ self,
88
+ cr_token_filename: Optional[str] = None,
89
+ iam_profile_name: Optional[str] = None,
90
+ iam_profile_id: Optional[str] = None,
91
+ url: Optional[str] = None,
92
+ client_id: Optional[str] = None,
93
+ client_secret: Optional[str] = None,
94
+ disable_ssl_verification: bool = False,
95
+ scope: Optional[str] = None,
96
+ proxies: Optional[Dict[str, str]] = None,
97
+ headers: Optional[Dict[str, str]] = None,
98
+ ) -> None:
95
99
  super().__init__(
96
- url=url, client_id=client_id, client_secret=client_secret,
97
- disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, scope=scope)
100
+ url=url,
101
+ client_id=client_id,
102
+ client_secret=client_secret,
103
+ disable_ssl_verification=disable_ssl_verification,
104
+ headers=headers,
105
+ proxies=proxies,
106
+ scope=scope,
107
+ )
98
108
 
99
109
  self.cr_token_filename = cr_token_filename
100
110
  self.iam_profile_name = iam_profile_name
101
111
  self.iam_profile_id = iam_profile_id
102
112
 
103
113
  self.request_payload['grant_type'] = 'urn:ibm:params:oauth:grant-type:cr-token'
114
+ self._set_user_agent(_build_user_agent('container-authenticator'))
104
115
 
105
116
  def retrieve_cr_token(self) -> str:
106
117
  """Retrieves the CR token for the current compute resource by reading it from the local file system.
107
118
 
108
119
  Raises:
109
- Exception: Cannot retrieve the compute resource token from.
120
+ Exception: Error retrieving the compute resource token.
110
121
 
111
122
  Returns:
112
123
  A string which contains the compute resource token.
113
124
  """
114
- cr_token_filename = self.cr_token_filename if self.cr_token_filename else self.DEFAULT_CR_TOKEN_FILENAME
115
-
116
- logger.debug('Attempting to read CR token from file: %s',
117
- cr_token_filename)
118
-
119
125
  try:
120
- with open(cr_token_filename, 'r', encoding='utf-8') as file:
121
- cr_token = file.read()
126
+ cr_token = None
127
+ if self.cr_token_filename:
128
+ # If the user specified a filename, then use that.
129
+ cr_token = self.read_file(self.cr_token_filename)
130
+ else:
131
+ # If the user didn't specify a filename, then try our two defaults.
132
+ try:
133
+ cr_token = self.read_file(self.DEFAULT_CR_TOKEN_FILENAME1)
134
+ except:
135
+ cr_token = self.read_file(self.DEFAULT_CR_TOKEN_FILENAME2)
122
136
  return cr_token
123
- # pylint: disable=broad-except
124
137
  except Exception as ex:
125
- raise Exception(
126
- 'Unable to retrieve the CR token value from file {}: {}'.format(cr_token_filename, ex)) from None
138
+ # pylint: disable=broad-exception-raised
139
+ raise Exception('Unable to retrieve the CR token: {}'.format(ex)) from None
127
140
 
128
141
  def request_token(self) -> dict:
129
142
  """Retrieves a CR token value from the current compute resource,
@@ -132,11 +145,9 @@ class ContainerTokenManager(IAMRequestBasedTokenManager):
132
145
  Returns:
133
146
  A dictionary containing the bearer token to be subsequently used service requests.
134
147
  """
135
- # Retrieve the CR token for this compute resource.
136
- cr_token = self.retrieve_cr_token()
137
148
 
138
149
  # Set the request payload.
139
- self.request_payload['cr_token'] = cr_token
150
+ self.request_payload['cr_token'] = self.retrieve_cr_token()
140
151
 
141
152
  if self.iam_profile_id:
142
153
  self.request_payload['profile_id'] = self.iam_profile_id
@@ -168,3 +179,22 @@ class ContainerTokenManager(IAMRequestBasedTokenManager):
168
179
  iam_profile_id: id of the linked trusted IAM profile to be used when obtaining the IAM access token
169
180
  """
170
181
  self.iam_profile_id = iam_profile_id
182
+
183
+ def read_file(self, filename: str) -> str:
184
+ """Read in the specified file and return the contents as a string.
185
+ Args:
186
+ filename: the name of the file to read
187
+ Returns:
188
+ The contents of the file as a string.
189
+ Raises:
190
+ Exception: An error occured reading the file.
191
+ """
192
+ try:
193
+ logger.debug('Attempting to read CR token from file: %s', filename)
194
+ with open(filename, 'r', encoding='utf-8') as file:
195
+ cr_token = file.read()
196
+ logger.debug('Successfully read CR token from file: %s', filename)
197
+ return cr_token
198
+ except Exception as ex:
199
+ # pylint: disable=broad-exception-raised
200
+ raise Exception('Error reading CR token from file {}: {}'.format(filename, ex)) from None
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- # Copyright 2019 IBM All Rights Reserved.
3
+ # Copyright 2019, 2024 IBM All Rights Reserved.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -17,8 +17,12 @@
17
17
  import json
18
18
  from typing import Dict, Optional
19
19
 
20
+ from ibm_cloud_sdk_core.logger import get_logger
21
+ from ..private_helpers import _build_user_agent
20
22
  from .jwt_token_manager import JWTTokenManager
21
23
 
24
+ logger = get_logger()
25
+
22
26
 
23
27
  class CP4DTokenManager(JWTTokenManager):
24
28
  """Token Manager of CloudPak for data.
@@ -48,19 +52,22 @@ class CP4DTokenManager(JWTTokenManager):
48
52
  proxies.https (str): The proxy endpoint to use for HTTPS requests.
49
53
  verify (str): The path to the certificate to use for HTTPS requests.
50
54
  """
55
+
51
56
  TOKEN_NAME = 'token'
52
57
  VALIDATE_AUTH_PATH = '/v1/authorize'
53
58
 
54
- def __init__(self,
55
- username: str = None,
56
- password: str = None,
57
- url: str = None,
58
- *,
59
- apikey: str = None,
60
- disable_ssl_verification: bool = False,
61
- headers: Optional[Dict[str, str]] = None,
62
- proxies: Optional[Dict[str, str]] = None,
63
- verify: Optional[str] = None) -> None:
59
+ def __init__(
60
+ self,
61
+ username: str = None,
62
+ password: str = None,
63
+ url: str = None,
64
+ *,
65
+ apikey: str = None,
66
+ disable_ssl_verification: bool = False,
67
+ headers: Optional[Dict[str, str]] = None,
68
+ proxies: Optional[Dict[str, str]] = None,
69
+ verify: Optional[str] = None,
70
+ ) -> None:
64
71
  self.username = username
65
72
  self.password = password
66
73
  self.verify = verify
@@ -72,23 +79,29 @@ class CP4DTokenManager(JWTTokenManager):
72
79
  self.headers = {}
73
80
  self.headers['Content-Type'] = 'application/json'
74
81
  self.proxies = proxies
75
- super().__init__(url, disable_ssl_verification=disable_ssl_verification,
76
- token_name=self.TOKEN_NAME)
82
+ super().__init__(url, disable_ssl_verification=disable_ssl_verification, token_name=self.TOKEN_NAME)
83
+ self._set_user_agent(_build_user_agent('cp4d-authenticator'))
77
84
 
78
85
  def request_token(self) -> dict:
79
- """Makes a request for a token.
80
- """
86
+ """Makes a request for a token."""
87
+ required_headers = {
88
+ 'User-Agent': self.user_agent,
89
+ }
90
+ request_headers = {}
91
+ if self.headers is not None and isinstance(self.headers, dict):
92
+ request_headers.update(self.headers)
93
+ request_headers.update(required_headers)
94
+
95
+ logger.debug('Invoking CP4D token service operation: %s', self.url)
81
96
  response = self._request(
82
97
  method='POST',
83
- headers=self.headers,
98
+ headers=request_headers,
84
99
  url=self.url,
85
- data=json.dumps({
86
- "username": self.username,
87
- "password": self.password,
88
- "api_key": self.apikey
89
- }),
100
+ data=json.dumps({"username": self.username, "password": self.password, "api_key": self.apikey}),
90
101
  proxies=self.proxies,
91
- verify=self.verify)
102
+ verify=self.verify,
103
+ )
104
+ logger.debug('Returned from CP4D token service operation')
92
105
  return response
93
106
 
94
107
  def set_headers(self, headers: Dict[str, str]) -> None:
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- # Copyright 2019 IBM All Rights Reserved.
3
+ # Copyright 2019, 2024 IBM All Rights Reserved.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -16,10 +16,13 @@
16
16
 
17
17
  from typing import Dict, Optional
18
18
 
19
+ from ibm_cloud_sdk_core.logger import get_logger
19
20
  from .jwt_token_manager import JWTTokenManager
20
21
 
22
+ logger = get_logger()
21
23
 
22
- #pylint: disable=too-many-instance-attributes
24
+
25
+ # pylint: disable=too-many-instance-attributes
23
26
  class IAMRequestBasedTokenManager(JWTTokenManager):
24
27
  """The IamRequestBasedTokenManager class contains code relevant to any token manager that
25
28
  interacts with the IAM service to manage a token. It stores information relevant to all
@@ -60,21 +63,25 @@ class IAMRequestBasedTokenManager(JWTTokenManager):
60
63
  scope: The "scope" to use when fetching the bearer token from the IAM token server.
61
64
  This can be used to obtain an access token with a specific scope.
62
65
  """
66
+
63
67
  DEFAULT_IAM_URL = 'https://iam.cloud.ibm.com'
64
68
  OPERATION_PATH = "/identity/token"
65
-
66
- def __init__(self,
67
- url: Optional[str] = None,
68
- client_id: Optional[str] = None,
69
- client_secret: Optional[str] = None,
70
- disable_ssl_verification: bool = False,
71
- headers: Optional[Dict[str, str]] = None,
72
- proxies: Optional[Dict[str, str]] = None,
73
- scope: Optional[str] = None) -> None:
69
+ IAM_EXPIRATION_WINDOW = 10
70
+
71
+ def __init__(
72
+ self,
73
+ url: Optional[str] = None,
74
+ client_id: Optional[str] = None,
75
+ client_secret: Optional[str] = None,
76
+ disable_ssl_verification: bool = False,
77
+ headers: Optional[Dict[str, str]] = None,
78
+ proxies: Optional[Dict[str, str]] = None,
79
+ scope: Optional[str] = None,
80
+ ) -> None:
74
81
  if not url:
75
82
  url = self.DEFAULT_IAM_URL
76
83
  if url.endswith(self.OPERATION_PATH):
77
- url = url[:-len(self.OPERATION_PATH)]
84
+ url = url[: -len(self.OPERATION_PATH)]
78
85
  self.url = url
79
86
  self.client_id = client_id
80
87
  self.client_secret = client_secret
@@ -83,8 +90,7 @@ class IAMRequestBasedTokenManager(JWTTokenManager):
83
90
  self.proxies = proxies
84
91
  self.scope = scope
85
92
  self.request_payload = {}
86
- super().__init__(
87
- self.url, disable_ssl_verification=disable_ssl_verification, token_name='access_token')
93
+ super().__init__(self.url, disable_ssl_verification=disable_ssl_verification, token_name='access_token')
88
94
 
89
95
  def request_token(self) -> dict:
90
96
  """Request an IAM OAuth token given an API Key.
@@ -95,12 +101,15 @@ class IAMRequestBasedTokenManager(JWTTokenManager):
95
101
  Returns:
96
102
  A dictionary containing the bearer token to be subsequently used service requests.
97
103
  """
98
- headers = {
99
- 'Content-type': 'application/x-www-form-urlencoded',
100
- 'Accept': 'application/json'
104
+ required_headers = {
105
+ 'Content-Type': 'application/x-www-form-urlencoded',
106
+ 'Accept': 'application/json',
107
+ 'User-Agent': self._get_user_agent(),
101
108
  }
109
+ request_headers = {}
102
110
  if self.headers is not None and isinstance(self.headers, dict):
103
- headers.update(self.headers)
111
+ request_headers.update(self.headers)
112
+ request_headers.update(required_headers)
104
113
 
105
114
  data = dict(self.request_payload)
106
115
 
@@ -112,13 +121,17 @@ class IAMRequestBasedTokenManager(JWTTokenManager):
112
121
  if self.client_id and self.client_secret:
113
122
  auth_tuple = (self.client_id, self.client_secret)
114
123
 
124
+ request_url = (self.url + self.OPERATION_PATH) if self.url else self.url
125
+ logger.debug('Invoking IAM get_token operation: %s', request_url)
115
126
  response = self._request(
116
127
  method='POST',
117
- url=(self.url + self.OPERATION_PATH) if self.url else self.url,
118
- headers=headers,
128
+ url=request_url,
129
+ headers=request_headers,
119
130
  data=data,
120
131
  auth_tuple=auth_tuple,
121
- proxies=self.proxies)
132
+ proxies=self.proxies,
133
+ )
134
+ logger.debug('Returned from IAM get_token operation')
122
135
  return response
123
136
 
124
137
  def set_client_id_and_secret(self, client_id: str, client_secret: str) -> None:
@@ -167,3 +180,19 @@ class IAMRequestBasedTokenManager(JWTTokenManager):
167
180
  value: A space seperated string that makes up the scope parameter.
168
181
  """
169
182
  self.scope = value
183
+
184
+ def _is_token_expired(self) -> bool:
185
+ """
186
+ Returns true iff the current cached token is expired.
187
+ We'll consider an access token as expired when we reach its IAM server-reported expiration time
188
+ minus our expiration window (10 secs).
189
+ We do this to avoid using an access token that might expire in the middle of a long-running transaction
190
+ within an IBM Cloud service.
191
+
192
+ Returns
193
+ -------
194
+ bool
195
+ True if token is expired; False otherwise
196
+ """
197
+ current_time = self._get_current_time()
198
+ return current_time >= (self.expire_time - self.IAM_EXPIRATION_WINDOW)