Skip to content

Commit

Permalink
feat: support api key use case for CP4D authenticator
Browse files Browse the repository at this point in the history
  • Loading branch information
rmkeezer committed May 12, 2021
1 parent 1fe1864 commit 830c28f
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 65 deletions.
22 changes: 19 additions & 3 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": "package-lock.json|^.secrets.baseline$",
"lines": null
},
"generated_at": "2021-03-18T14:18:04Z",
"generated_at": "2021-05-04T15:09:09Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -173,9 +173,17 @@
"hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2",
"is_secret": false,
"is_verified": false,
"line_number": 112,
"line_number": 113,
"type": "Hex High Entropy String",
"verified_result": null
},
{
"hashed_secret": "5eb942810a75ebc850972a89285d570d484c89c4",
"is_secret": false,
"is_verified": false,
"line_number": 307,
"type": "Secret Keyword",
"verified_result": null
}
],
"test/test_cp4d_authenticator.py": [
Expand All @@ -186,6 +194,14 @@
"line_number": 88,
"type": "Hex High Entropy String",
"verified_result": null
},
{
"hashed_secret": "5eb942810a75ebc850972a89285d570d484c89c4",
"is_secret": false,
"is_verified": false,
"line_number": 99,
"type": "Secret Keyword",
"verified_result": null
}
],
"test/test_cp4d_token_manager.py": [
Expand Down Expand Up @@ -287,7 +303,7 @@
}
]
},
"version": "0.13.1+ibm.29.dss",
"version": "0.13.1+ibm.34.dss",
"word_list": {
"file": null,
"hash": null
Expand Down
27 changes: 22 additions & 5 deletions Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ service = ExampleService(authenticator=authenticator)
```

## Cloud Pak for Data
The `CloudPakForDataAuthenticator` will accept user-supplied username and password values, and will
The `CloudPakForDataAuthenticator` will accept a user-supplied username and either a password or an apikey value, and will
perform the necessary interactions with the Cloud Pak for Data token service to obtain a suitable
bearer token. The authenticator will also obtain a new bearer token when the current token expires.
The bearer token is then added to each outbound request in the `Authorization` header in the
Expand All @@ -150,8 +150,9 @@ form:
```
### Properties
- username: (required) the username used to obtain a bearer token.
- password: (required) the password used to obtain a bearer token.
- password: (required if apikey is not specified) the password used to obtain a bearer token.
- url: (required) The URL representing the Cloud Pak for Data token service endpoint.
- apikey: (required if password is not specified) the apikey used to obtain a bearer token.
- disableSSLVerification: (optional) A flag that indicates whether verificaton of the server's SSL
certificate should be disabled or not. The default value is `false`.
- headers: (optional) A set of key/value pairs that will be sent as HTTP headers in requests
Expand All @@ -160,22 +161,38 @@ made to the IAM token service.
```python
from ibm_cloud_sdk_core.authenticators import CloudPakForDataAuthenticator

# Username / password authentication
authenticator = CloudPakForDataAuthenticator(
'my_username',
'my_password',
'https://my-cp4d-url',
username='my_username',
password='my_password',
url='https://my-cp4d-url',
disable_ssl_verification=True)

# Username / apikey authentication
authenticator = CloudPakForDataAuthenticator(
username='my_username',
apikey='my_apikey',
url='https://my-cp4d-url',
disable_ssl_verification=True)

service = ExampleService(authenticator=authenticator)

service.get_authenticator().set_headers({'dummy': 'headers'})
```
### Configuration example
External configuration:
```
# Username / password authentication
export EXAMPLE_SERVICE_AUTH_TYPE=cp4d
export EXAMPLE_SERVICE_USERNAME=myuser
export EXAMPLE_SERVICE_PASSWORD=mypassword
export EXAMPLE_SERVICE_URL=https://mycp4dhost.com/
# Username / apikey authentication
export EXAMPLE_SERVICE_AUTH_TYPE=cp4d
export EXAMPLE_SERVICE_USERNAME=myuser
export EXAMPLE_SERVICE_APIKEY=myapikey
export EXAMPLE_SERVICE_URL=https://mycp4dhost.com/
```
Application code:
```python
Expand Down
30 changes: 17 additions & 13 deletions ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ class CloudPakForDataAuthenticator(Authenticator):
Authorization: Bearer <bearer-token>
Args:
username: The username used to obtain a bearer token.
password: The password used to obtain a bearer token.
url: The URL representing the Cloud Pak for Data token service endpoint.
Keyword Args:
username: The username used to obtain a bearer token [required].
password: The password used to obtain a bearer token [required if apikey not specified].
url: The URL representing the Cloud Pak for Data token service endpoint [required].
apikey: The API key used to obtain a bearer token [required if password not specified].
disable_ssl_verification: A flag that indicates whether verification of the server's SSL
certificate should be disabled or not. Defaults to False.
headers: Default headers to be sent with every CP4D token request. Defaults to None.
Expand All @@ -48,21 +47,22 @@ class CloudPakForDataAuthenticator(Authenticator):
token_manager (CP4DTokenManager): Retrives and manages CP4D tokens from the endpoint specified by the url.
Raises:
ValueError: The username, password, and/or url are not valid for CP4D token requests.
ValueError: The username, password/apikey, and/or url are not valid for CP4D token requests.
"""
authenticationdict = 'cp4d'

def __init__(self,
username: str,
password: str,
url: str,
username: str = None,
password: str = None,
url: str = None,
*,
apikey: str = None,
disable_ssl_verification: bool = False,
headers: Optional[Dict[str, str]] = None,
proxies: Optional[Dict[str, str]] = None) -> None:
self.token_manager = CP4DTokenManager(
username, password, url, disable_ssl_verification=disable_ssl_verification,
headers=headers, proxies=proxies)
username=username, password=password, apikey=apikey, url=url,
disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies)
self.validate()

def validate(self) -> None:
Expand All @@ -74,8 +74,12 @@ def validate(self) -> None:
Raises:
ValueError: The username, password, and/or url are not valid for token requests.
"""
if self.token_manager.username is None or self.token_manager.password is None:
raise ValueError('The username and password shouldn\'t be None.')
if self.token_manager.username is None:
raise ValueError('The username shouldn\'t be None.')

if ((self.token_manager.password is None and self.token_manager.apikey is None)
or (self.token_manager.password is not None and self.token_manager.apikey is not None)):
raise ValueError('Exactly one of `apikey` or `password` must be specified.')

if self.token_manager.url is None:
raise ValueError('The url shouldn\'t be None.')
Expand Down
37 changes: 22 additions & 15 deletions ibm_cloud_sdk_core/cp4d_token_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import json
from typing import Dict, Optional
from .jwt_token_manager import JWTTokenManager

Expand All @@ -24,12 +25,11 @@ class CP4DTokenManager(JWTTokenManager):
The Token Manager performs basic auth with a username and password
to acquire JWT tokens.
Args:
username: The username for authentication.
password: The password for authentication.
url: The endpoint for JWT token requests.
Keyword Arguments:
username: The username for authentication [required].
password: The password for authentication [required if apikey not specified].
url: The endpoint for JWT token requests [required].
apikey: The apikey for authentication [required if password not specified].
disable_ssl_verification: Disable ssl verification. Defaults to False.
headers: Headers to be sent with every service token request. Defaults to None.
proxies: Proxies to use for making request. Defaults to None.
Expand All @@ -45,36 +45,43 @@ class CP4DTokenManager(JWTTokenManager):
proxies.http (str): The proxy endpoint to use for HTTP requests.
proxies.https (str): The proxy endpoint to use for HTTPS requests.
"""
TOKEN_NAME = 'accessToken'
VALIDATE_AUTH_PATH = '/v1/preauth/validateAuth'
TOKEN_NAME = 'token'
VALIDATE_AUTH_PATH = '/v1/authorize'

def __init__(self,
username: str,
password: str,
url: str,
username: str = None,
password: str = None,
url: str = None,
*,
apikey: str = None,
disable_ssl_verification: bool = False,
headers: Optional[Dict[str, str]] = None,
proxies: Optional[Dict[str, str]] = None) -> None:
self.username = username
self.password = password
if url and not self.VALIDATE_AUTH_PATH in url:
url = url + '/v1/preauth/validateAuth'
url = url + '/v1/authorize'
self.apikey = apikey
self.headers = headers
if self.headers is None:
self.headers = {}
self.headers['Content-Type'] = 'application/json'
self.proxies = proxies
super().__init__(url, disable_ssl_verification=disable_ssl_verification,
token_name=self.TOKEN_NAME)

def request_token(self) -> dict:
"""Makes a request for a token.
"""
auth_tuple = (self.username, self.password)

response = self._request(
method='GET',
method='POST',
headers=self.headers,
url=self.url,
auth_tuple=auth_tuple,
data=json.dumps({
"username": self.username,
"password": self.password,
"api_key": self.apikey
}),
proxies=self.proxies)
return response

Expand Down
1 change: 1 addition & 0 deletions ibm_cloud_sdk_core/get_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def __construct_authenticator(config: dict) -> Authenticator:
username=config.get('USERNAME'),
password=config.get('PASSWORD'),
url=config.get('AUTH_URL'),
apikey=config.get('APIKEY'),
disable_ssl_verification=config.get('AUTH_DISABLE_SSL'))
elif auth_type == 'iam' and config.get('APIKEY'):
authenticator = IAMAuthenticator(
Expand Down
11 changes: 11 additions & 0 deletions resources/ibm-credentials-cp4dtest.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CP4D_PASSWORD_TEST_AUTH_URL=<url> e.g. https://cpd350-cpd-cpd350.apps.wml-kf-cluster.os.fyre.ibm.com/icp4d-api
CP4D_PASSWORD_TEST_AUTH_TYPE=cp4d
CP4D_PASSWORD_TEST_USERNAME=<username>
CP4D_PASSWORD_TEST_PASSWORD=<password>
CP4D_PASSWORD_TEST_AUTH_DISABLE_SSL=true

CP4D_APIKEY_TEST_AUTH_URL=<url> e.g. https://cpd350-cpd-cpd350.apps.wml-kf-cluster.os.fyre.ibm.com/icp4d-api
CP4D_APIKEY_TEST_AUTH_TYPE=cp4d
CP4D_APIKEY_TEST_USERNAME=<username>
CP4D_APIKEY_TEST_APIKEY=<apikey>
CP4D_APIKEY_TEST_AUTH_DISABLE_SSL=true
32 changes: 16 additions & 16 deletions test/test_base_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ def test_for_cp4d():
assert service.authenticator.token_manager is not None
assert service.authenticator.token_manager.username == 'my_username'
assert service.authenticator.token_manager.password == 'my_password'
assert service.authenticator.token_manager.url == 'my_url/v1/preauth/validateAuth'
assert service.authenticator.token_manager.url == 'my_url/v1/authorize'
assert isinstance(service.authenticator.token_manager, CP4DTokenManager)


Expand Down Expand Up @@ -648,10 +648,10 @@ def test_files_dict():
service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator())

form_data = {}
file = open(
with open(
os.path.join(
os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r')
form_data['file1'] = (None, file, 'application/octet-stream')
os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r') as file:
form_data['file1'] = (None, file, 'application/octet-stream')
form_data['string1'] = (None, 'hello', 'text/plain')
request = service.prepare_request('GET', url='', headers={'X-opt-out': True}, files=form_data)
files = request['files']
Expand All @@ -667,10 +667,10 @@ def test_files_list():
service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator())

form_data = []
file = open(
with open(
os.path.join(
os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r')
form_data.append(('file1', (None, file, 'application/octet-stream')))
os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r') as file:
form_data.append(('file1', (None, file, 'application/octet-stream')))
form_data.append(('string1', (None, 'hello', 'text/plain')))
request = service.prepare_request('GET', url='', headers={'X-opt-out': True}, files=form_data)
files = request['files']
Expand All @@ -686,18 +686,18 @@ def test_files_duplicate_parts():
service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator())

form_data = []
file = open(
with open(
os.path.join(
os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r')
form_data.append(('creds_file', (None, file, 'application/octet-stream')))
file = open(
os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r') as file:
form_data.append(('creds_file', (None, file, 'application/octet-stream')))
with open(
os.path.join(
os.path.dirname(__file__), '../resources/ibm-credentials-basic.env'), 'r')
form_data.append(('creds_file', (None, file, 'application/octet-stream')))
file = open(
os.path.dirname(__file__), '../resources/ibm-credentials-basic.env'), 'r') as file:
form_data.append(('creds_file', (None, file, 'application/octet-stream')))
with open(
os.path.join(
os.path.dirname(__file__), '../resources/ibm-credentials-bearer.env'), 'r')
form_data.append(('creds_file', (None, file, 'application/octet-stream')))
os.path.dirname(__file__), '../resources/ibm-credentials-bearer.env'), 'r') as file:
form_data.append(('creds_file', (None, file, 'application/octet-stream')))
request = service.prepare_request('GET', url='', headers={'X-opt-out': True}, files=form_data)
files = request['files']
assert isinstance(files, list)
Expand Down
Loading

0 comments on commit 830c28f

Please sign in to comment.