Skip to content

Change order of credential path and read_external_sources and more #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Sep 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ibm_cloud_sdk_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
from .jwt_token_manager import JWTTokenManager
from .cp4d_token_manager import CP4DTokenManager
from .api_exception import ApiException
from .utils import datetime_to_string, string_to_datetime, get_authenticator_from_environment
from .utils import datetime_to_string, string_to_datetime, get_authenticator_from_environment, read_external_sources
19 changes: 4 additions & 15 deletions ibm_cloud_sdk_core/base_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import requests
from requests.structures import CaseInsensitiveDict
from .version import __version__
from .utils import has_bad_first_or_last_char, remove_null_values, cleanup_values, read_from_env_variables
from .utils import has_bad_first_or_last_char, remove_null_values, cleanup_values, read_external_sources
from .detailed_response import DetailedResponse
from .api_exception import ApiException
from .authenticators import Authenticator
Expand All @@ -37,15 +37,13 @@ class BaseService(object):
SDK_NAME = 'ibm-python-sdk-core'

def __init__(self,
service_url,
service_url=None,
authenticator=None,
disable_ssl_verification=False,
display_name=None):
disable_ssl_verification=False):
"""
:attr str url: The url for service api calls
:attr str service_url: The url for service api calls
:attr Authenticator authenticator: The authenticator for authentication
:attr bool disable_ssl_verification: enables/ disables ssl verification
:attr str display_name the name used for mapping services in environment file
"""
self.service_url = service_url
self.http_config = {}
Expand All @@ -63,15 +61,6 @@ def __init__(self,
raise ValueError(
'authenticator should be of type Authenticator')

if display_name:
service_name = display_name.replace(' ', '_').lower()
config = read_from_env_variables(service_name)
if config.get('url'):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting of client properties go in the generated service classes

self.service_url = config.get('url')
if config.get('disable_ssl'):
self.disable_ssl_verification = config.get('disable_ssl')


def _get_system_info(self):
return '{0} {1} {2}'.format(
platform.system(), # OS
Expand Down
109 changes: 59 additions & 50 deletions ibm_cloud_sdk_core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,62 +54,71 @@ def string_to_datetime(string):
"""
return date_parser.parse(string)

def get_authenticator_from_environment(service_name):
def read_external_sources(service_name):
"""
Checks the credentials file and VCAP_SERVICES environment variable
Try to get config from external sources, with the following priority:
1. Credentials file(ibm-credentials.env)
2. Environment variables
3. VCAP Services(Cloud Foundry)
:param service_name: The service name
:return: the authenticator
:return: dict
"""
authenticator = None
# 1. Credentials from credential file
config = {}

config = read_from_credential_file(service_name)
if config:
authenticator = contruct_authenticator(config)

# 2. From env variables
if not authenticator:
if not config:
config = read_from_env_variables(service_name)
if config:
authenticator = contruct_authenticator(config)

# 3. Credentials from VCAP
if not authenticator:
if not config:
config = read_from_vcap_services(service_name)
if config:
authenticator = contruct_authenticator(config)

return config

def get_authenticator_from_environment(service_name):
"""
Try to get authenticator from external sources, with the following priority:
1. Credentials file(ibm-credentials.env)
2. Environment variables
3. VCAP Services(Cloud Foundry)
:param service_name: The service name
:return: the authenticator
"""
authenticator = None
config = read_external_sources(service_name)
if config:
authenticator = _construct_authenticator(config)
return authenticator

def read_from_env_variables(service_name):
"""
:return dict config: parsed env variables
"""
service_name = service_name.replace(' ', '_').lower()
config = {}
for key, value in environ.items():
_parse_key_and_update_config(config, service_name.lower(), key.lower(), value)
_parse_key_and_update_config(config, service_name, key, value)
return config

def read_from_credential_file(service_name, separator='='):
"""
:param str service_name: The service name
:return dict config: parsed key values pairs
"""
service_name = service_name.replace(' ', '_').lower()
DEFAULT_CREDENTIALS_FILE_NAME = 'ibm-credentials.env'

# File path specified by an env variable
credential_file_path = getenv('IBM_CREDENTIALS_FILE')

# Home directory
# Current working directory
if credential_file_path is None:
file_path = join(expanduser('~'), DEFAULT_CREDENTIALS_FILE_NAME)
file_path = join(
dirname(dirname(abspath(__file__))), DEFAULT_CREDENTIALS_FILE_NAME)
if isfile(file_path):
credential_file_path = file_path

# Top-level of the project directory
# Home directory
if credential_file_path is None:
file_path = join(
dirname(dirname(abspath(__file__))), DEFAULT_CREDENTIALS_FILE_NAME)
file_path = join(expanduser('~'), DEFAULT_CREDENTIALS_FILE_NAME)
if isfile(file_path):
credential_file_path = file_path

Expand All @@ -119,62 +128,62 @@ def read_from_credential_file(service_name, separator='='):
for line in fp:
key_val = line.strip().split(separator)
if len(key_val) == 2:
key = key_val[0].lower()
key = key_val[0]
value = key_val[1]
_parse_key_and_update_config(config, service_name, key, value)
return config

def _parse_key_and_update_config(config, service_name, key, value):
if service_name in key:
index = key.find('_')
if index != -1:
config[key[index + 1:]] = value
service_name = service_name.replace(' ', '_').replace('-', '_').upper()
if key.startswith(service_name):
config[key[len(service_name) + 1:]] = value

def read_from_vcap_services(service_name):
service_name = service_name.replace(' ', '_').lower()
vcap_services = getenv('VCAP_SERVICES')
vcap_service_credentials = None
vcap_service_credentials = {}
if vcap_services:
services = json_import.loads(vcap_services)

for key in services.keys():
name = key.replace('-', '_')
if name == service_name:
if key == service_name:
vcap_service_credentials = services[key][0]['credentials']
if vcap_service_credentials is not None and isinstance(vcap_service_credentials, dict):
if vcap_service_credentials.get('username') and vcap_service_credentials.get('password'): # cf
vcap_service_credentials['auth_type'] = 'basic'
vcap_service_credentials['AUTH_TYPE'] = 'basic'
vcap_service_credentials['USERNAME'] = vcap_service_credentials.get('username')
vcap_service_credentials['PASSWORD'] = vcap_service_credentials.get('password')
elif vcap_service_credentials.get('apikey'): # rc
vcap_service_credentials['auth_type'] = 'iam'
vcap_service_credentials['AUTH_TYPE'] = 'iam'
vcap_service_credentials['APIKEY'] = vcap_service_credentials.get('apikey')
else: # no other auth mechanism is supported
vcap_service_credentials = None
vcap_service_credentials = {}
return vcap_service_credentials

def contruct_authenticator(config):
auth_type = config.get('auth_type').lower() if config.get('auth_type') else 'iam'
def _construct_authenticator(config):
auth_type = config.get('AUTH_TYPE').lower() if config.get('AUTH_TYPE') else 'iam'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't comment on this before, and I don't think we need to change anything right now, but... when we implement the service config feature, it might be good to define constants for the various config properties rather than use the literal strings like this.

authenticator = None
from .authenticators import BasicAuthenticator, BearerTokenAuthenticator, CloudPakForDataAuthenticator, IAMAuthenticator, NoAuthAuthenticator

if auth_type == 'basic':
authenticator = BasicAuthenticator(
username=config.get('username'),
password=config.get('password'))
username=config.get('USERNAME'),
password=config.get('PASSWORD'))
elif auth_type == 'bearertoken':
authenticator = BearerTokenAuthenticator(
bearer_token=config.get('bearer_token'))
bearer_token=config.get('BEARER_TOKEN'))
elif auth_type == 'cp4d':
authenticator = CloudPakForDataAuthenticator(
username=config.get('username'),
password=config.get('password'),
url=config.get('auth_url'),
disable_ssl_verification=config.get('auth_disable_ssl'))
elif auth_type == 'iam' and config.get('apikey'):
username=config.get('USERNAME'),
password=config.get('PASSWORD'),
url=config.get('AUTH_URL'),
disable_ssl_verification=config.get('AUTH_DISABLE_SSL'))
elif auth_type == 'iam' and config.get('APIKEY'):
authenticator = IAMAuthenticator(
apikey=config.get('apikey'),
url=config.get('auth_url'),
client_id=config.get('client_id'),
client_secret=config.get('client_secret'),
disable_ssl_verification=config.get('auth_disable_ssl'))
apikey=config.get('APIKEY'),
url=config.get('AUTH_URL'),
client_id=config.get('CLIENT_ID'),
client_secret=config.get('CLIENT_SECRET'),
disable_ssl_verification=config.get('AUTH_DISABLE_SSL'))
elif auth_type == 'noauth':
authenticator = NoAuthAuthenticator()

Expand Down
6 changes: 4 additions & 2 deletions resources/ibm-credentials-iam.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
WATSON_APIKEY=5678efgh
WATSON_AUTH_TYPE=iam
IBM_WATSON_APIKEY=5678efgh
IBM_WATSON_AUTH_TYPE=iam
IBM_WATSON_URL=https://gateway-s.watsonplatform.net/watson/api
IBM_WATSON_DISABLE_SSL=False
18 changes: 7 additions & 11 deletions test/test_base_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from ibm_cloud_sdk_core import ApiException
from ibm_cloud_sdk_core import CP4DTokenManager
from ibm_cloud_sdk_core.authenticators import IAMAuthenticator, NoAuthAuthenticator, Authenticator, BasicAuthenticator, CloudPakForDataAuthenticator
from ibm_cloud_sdk_core import get_authenticator_from_environment


class AnyServiceV1(BaseService):
Expand All @@ -23,8 +24,7 @@ def __init__(self,
self,
service_url=service_url,
authenticator=authenticator,
disable_ssl_verification=disable_ssl_verification,
display_name='Watson')
disable_ssl_verification=disable_ssl_verification)
self.version = version

def op_with_path_params(self, path0, path1):
Expand Down Expand Up @@ -135,17 +135,13 @@ def test_fail_http_config():

@responses.activate
def test_iam():
iam_authenticator = IAMAuthenticator('my_apikey', 'https://iam-test.cloud.ibm.com/identity/token')
file_path = os.path.join(
os.path.dirname(__file__), '../resources/ibm-credentials-iam.env')
os.environ['IBM_CREDENTIALS_FILE'] = file_path
os.environ['WATSON_URL'] = 'https://gateway-s.watsonplatform.net/watson/api'
os.environ['WATSON_DISABLE_SSL'] = 'False'
iam_authenticator = get_authenticator_from_environment('ibm-watson')
service = AnyServiceV1('2017-07-07', authenticator=iam_authenticator)
assert service.service_url == 'https://gateway-s.watsonplatform.net/watson/api'
assert service.service_url == 'https://gateway.watsonplatform.net/test/api'
del os.environ['IBM_CREDENTIALS_FILE']
del os.environ['WATSON_URL']
del os.environ['WATSON_DISABLE_SSL']
assert service.authenticator is not None

response = {
Expand All @@ -157,12 +153,12 @@ def test_iam():
}
responses.add(
responses.POST,
url='https://iam-test.cloud.ibm.com/identity/token',
url='https://iam.cloud.ibm.com/identity/token',
body=json.dumps(response),
status=200)
responses.add(
responses.GET,
url='https://gateway-s.watsonplatform.net/watson/api',
url='https://gateway.watsonplatform.net/test/api',
body=json.dumps({
"foobar": "baz"
}),
Expand Down Expand Up @@ -422,7 +418,7 @@ def test_json():
assert req.get('data') == "{\"hello\": \"world\"}"

def test_service_url_not_set():
service = BaseService(service_url='', authenticator=NoAuthAuthenticator(), display_name='Watson')
service = BaseService(service_url='', authenticator=NoAuthAuthenticator())
with pytest.raises(ValueError) as err:
service.prepare_request('POST', url='')
assert str(err.value) == 'The service_url is required'
13 changes: 10 additions & 3 deletions test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ def test_datetime_conversion():

def test_get_authenticator_from_credential_file():
file_path = os.path.join(
os.path.dirname(__file__), '../resources/ibm-credentials-iam.env')
os.path.dirname(__file__), '../resources/ibm-credentials-iam.env')
os.environ['IBM_CREDENTIALS_FILE'] = file_path
authenticator = get_authenticator_from_environment('watson')
authenticator = get_authenticator_from_environment('ibm watson')
assert authenticator is not None
assert authenticator.token_manager.apikey == '5678efgh'
del os.environ['IBM_CREDENTIALS_FILE']
Expand Down Expand Up @@ -48,7 +48,7 @@ def test_get_authenticator_from_credential_file():
assert authenticator.bearer_token is not None
del os.environ['IBM_CREDENTIALS_FILE']

def test_get_authenticator_from_env_variabled():
def test_get_authenticator_from_env_variables():
os.environ['TEST_APIKEY'] = '5678efgh'
authenticator = get_authenticator_from_environment('test')
assert authenticator is not None
Expand Down Expand Up @@ -86,3 +86,10 @@ def test_vcap_credentials():
authenticator = get_authenticator_from_environment('test')
assert authenticator is None
del os.environ['VCAP_SERVICES']

def test_multi_word_service_name():
os.environ['PERSONALITY_INSIGHTS_APIKEY'] = '5678efgh'
authenticator = get_authenticator_from_environment('personality-insights')
assert authenticator is not None
assert authenticator.token_manager.apikey == '5678efgh'
del os.environ['PERSONALITY_INSIGHTS_APIKEY']