diff --git a/setup.py b/setup.py index 34f5560ea..d790c7f7f 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ 'pycrypto', 'python-ldap==2.4.25', 'PyYAML', - 'umapi-client>=2.2', + 'umapi-client>=2.3', 'psutil', 'keyring' ], diff --git a/user_sync/config.py b/user_sync/config.py index a9d290078..94f6a1163 100644 --- a/user_sync/config.py +++ b/user_sync/config.py @@ -27,7 +27,6 @@ import user_sync.identity_type import user_sync.rules -from user_sync import credential_manager from user_sync.error import AssertionException DEFAULT_MAIN_CONFIG_FILENAME = 'user-sync-config.yml' @@ -139,20 +138,10 @@ def get_directory_connector_options(self, connector_name): ''' options = {} connectors_config = self.get_directory_connector_configs() - if (connectors_config != None): + if connectors_config is not None: connector_item = connectors_config.get_list(connector_name, True) options = self.get_dict_from_sources(connector_item) - options = self.combine_dicts([options, self.options['directory_connector_overridden_options']]) - # credentials are None, a dict, or a config filename to read to get a dict - credentials = credential_manager.get_credentials(credential_manager.DIRECTORY_CREDENTIAL_TYPE, - connector_name, - config=options, - config_loader = self) - if isinstance(credentials, types.StringTypes): - credentials = ConfigFileLoader.load_other_config(credentials) - if isinstance(credentials, dict): - options = self.combine_dicts([options, credentials]) return options def get_directory_groups(self): @@ -164,9 +153,9 @@ def get_directory_groups(self): raise AssertionException("Your main configuration file is still in v1 format. Please convert it to v2.") groups_config = None directory_config = self.main_config.get_dict_config('directory_users', True) - if (directory_config != None): + if directory_config is not None: groups_config = directory_config.get_list_config('groups', True) - if (groups_config == None): + if groups_config is None: return adobe_groups_by_directory_group for item in groups_config.iter_dict_configs(): @@ -215,7 +204,6 @@ def get_dict_from_sources(self, sources): Given a list of config file paths, return the dictionary composed of all the contents of those config files, or None if the list is empty :param sources: a list of strings - :param owner: a string to use in error messages if we can't find a config file. :rtype dict ''' if not sources: @@ -249,10 +237,10 @@ def combine_dicts(dicts): ''' result = {} for dict_item in dicts: - if (isinstance(dict_item, dict)): + if isinstance(dict_item, dict): for dict_key, dict_item in dict_item.iteritems(): result_item = result.get(dict_key) - if (isinstance(result_item, dict) and isinstance(dict_item, dict)): + if isinstance(result_item, dict) and isinstance(dict_item, dict): result_item.update(dict_item) else: result[dict_key] = dict_item @@ -357,19 +345,6 @@ def get_rule_options(self): def create_umapi_options(self, connector_config_sources): options = self.get_dict_from_sources(connector_config_sources) options['test_mode'] = self.options['test_mode'] - enterprise_section = options.get('enterprise') - if isinstance(enterprise_section, dict): - org_id = enterprise_section.get('org_id') - if (org_id != None): - # credentials are None, a dict, or a config filename to read to get a dict - credentials = credential_manager.get_credentials(credential_manager.UMAPI_CREDENTIAL_TYPE, - org_id, - config = enterprise_section, - config_loader = self) - if isinstance(credentials, types.StringTypes): - credentials = ConfigFileLoader.load_other_config(credentials) - if isinstance(credentials, dict): - options['enterprise'] = self.combine_dicts([enterprise_section, credentials]) return options def check_unused_config_keys(self): @@ -420,9 +395,9 @@ def create_assertion_error(self, message): return AssertionException("%s in: %s" % (message, self.get_full_scope())) def describe_types(self, types_to_describe): - if (types_to_describe == types.StringTypes): + if types_to_describe == types.StringTypes: result = self.describe_types(types.StringType) - elif (isinstance(types_to_describe, tuple)): + elif isinstance(types_to_describe, tuple): result = [] for type_to_describe in types_to_describe: result.extend(self.describe_types(type_to_describe)) @@ -430,12 +405,13 @@ def describe_types(self, types_to_describe): result = [types_to_describe.__name__] return result - def report_unused_values(self, logger, optional_configs = []): + def report_unused_values(self, logger, optional_configs=None): + optional_configs = [] if optional_configs is None else optional_configs has_error = False for config in self.iter_configs(): messages = config.describe_unused_values() - if (len(messages) > 0): - if (config in optional_configs): + if len(messages) > 0: + if config in optional_configs: log_level = logging.WARNING else: log_level = logging.ERROR @@ -443,7 +419,7 @@ def report_unused_values(self, logger, optional_configs = []): for message in messages: logger.log(log_level, message) - if (has_error): + if has_error: raise AssertionException('Detected unused keys that are not ignorable.') def describe_unused_values(self): @@ -465,7 +441,7 @@ def iter_values(self, allowed_types): ''' index = 0 for item in self.value: - if (not isinstance(item, allowed_types)): + if not isinstance(item, allowed_types): reported_types = self.describe_types(allowed_types) raise self.create_assertion_error("Value should be one of these types: %s for index: %s" % (reported_types, index)) index += 1 @@ -475,7 +451,7 @@ def iter_dict_configs(self): index = 0 for value in self.iter_values(dict): config = self.find_child_config(index) - if (config == None): + if config is None: config = DictConfig("[%s]" % index, value) self.add_child(config) yield config @@ -500,7 +476,7 @@ def iter_keys(self): def iter_unused_keys(self): for key in self.iter_keys(): - if (key not in self.accessed_keys): + if key not in self.accessed_keys: yield key def get_dict_config(self, key, none_allowed = False): @@ -508,9 +484,9 @@ def get_dict_config(self, key, none_allowed = False): :rtype DictConfig ''' result = self.find_child_config(key) - if (result == None): + if result is None: value = self.get_dict(key, none_allowed) - if (value != None): + if value is not None: result = DictConfig(key, value) self.add_child(result) return result @@ -530,7 +506,7 @@ def get_bool(self, key, none_allowed = False): def get_list(self, key, none_allowed = False): value = self.get_value(key, None, none_allowed) - if (value != None and not isinstance(value, list)): + if value is not None and not isinstance(value, list): value = [value] return value @@ -539,9 +515,9 @@ def get_list_config(self, key, none_allowed = False): :rtype ListConfig ''' result = self.find_child_config(key) - if (result == None): + if result is None: value = self.get_list(key, none_allowed) - if (value != None): + if value is not None: result = ListConfig(key, value) self.add_child(result) return result @@ -554,10 +530,10 @@ def get_value(self, key, allowed_types, none_allowed = False): ''' self.accessed_keys.add(key) result = self.value.get(key) - if (result == None): - if (not none_allowed): + if result is None: + if not none_allowed: raise self.create_assertion_error("Value not found for key: %s" % key) - elif (allowed_types != None and not isinstance(result, allowed_types)): + elif allowed_types is not None and not isinstance(result, allowed_types): reported_types = self.describe_types(allowed_types) raise self.create_assertion_error("Value should be one of these types: %s for key: %s" % (reported_types, key)) return result @@ -565,7 +541,7 @@ def get_value(self, key, allowed_types, none_allowed = False): def describe_unused_values(self): messages = [] unused_keys = list(self.iter_unused_keys()) - if (len(unused_keys) > 0): + if len(unused_keys) > 0: messages.append("Found unused keys: %s in: %s" % (unused_keys, self.get_full_scope())) return messages @@ -717,8 +693,9 @@ def process_path_value(cls, val, must_exist, can_have_subdict): does the relative path processing for a value from the dictionary, which can be a string, a list of strings, or a list of strings and "tagged" strings (sub-dictionaries whose values are strings) - :param key: the key whose value we are processing, for error messages :param val: the value we are processing, for error messages + :param must_exist: whether there must be a value + :param can_have_subdict: whether the value can be a tagged string ''' if isinstance(val, types.StringTypes): return cls.relative_path(val, must_exist) @@ -793,7 +770,7 @@ def set_value(self, key, allowed_types, default_value): ''' value = default_value config = self.default_config - if (config != None and config.has_key(key)): + if config is not None and config.has_key(key): value = config.get_value(key, allowed_types, False) self.options[key] = value @@ -809,7 +786,7 @@ def require_value(self, key, allowed_types): :type key: str ''' config = self.default_config - if (config == None): + if config is None: raise AssertionException("No config found.") self.options[key] = value = config.get_value(key, allowed_types) return value diff --git a/user_sync/connector/directory_csv.py b/user_sync/connector/directory_csv.py index 811f13cf5..eaad7a725 100644 --- a/user_sync/connector/directory_csv.py +++ b/user_sync/connector/directory_csv.py @@ -68,15 +68,13 @@ def __init__(self, caller_options): builder.set_string_value('logger_name', CSVDirectoryConnector.name) builder.require_string_value('file_path') options = builder.get_options() - - # identity type for new users if not specified in column - self.user_identity_type = user_sync.identity_type.parse_identity_type(options['user_identity_type']) - self.options = options - self.logger = logger = user_sync.connector.helper.create_logger(options) + self.logger = logger = user_sync.connector.helper.create_logger(options) + logger.debug('CSV initialized with options: %s', options) caller_config.report_unused_values(logger) - logger.debug('Initialized with options: %s', options) + # identity type for new users if not specified in column + self.user_identity_type = user_sync.identity_type.parse_identity_type(options['user_identity_type']) def load_users_and_groups(self, groups, extended_attributes): ''' @@ -129,7 +127,7 @@ def get_column_name(key): email = self.get_column_value(row, email_column_name) if email is None or email.find('@') < 0: logger.warning('Missing or invalid email at row: %d; skipping', line_read) - continue; + continue user = users.get(email) if user is None: diff --git a/user_sync/connector/directory_ldap.py b/user_sync/connector/directory_ldap.py index 8671c341f..6a5ed60ef 100755 --- a/user_sync/connector/directory_ldap.py +++ b/user_sync/connector/directory_ldap.py @@ -18,13 +18,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import ldap.controls.libldap import string +import keyring +import ldap.controls.libldap + import user_sync.config import user_sync.connector.helper import user_sync.error import user_sync.identity_type +from user_sync.error import AssertionException + def connector_metadata(): metadata = { @@ -70,35 +74,52 @@ def __init__(self, caller_options): host = builder.require_string_value('host') username = builder.require_string_value('username') builder.require_string_value('base_dn') - options = builder.get_options() - password = caller_config.get_string('password') - # make sure credential_manager does not get reported as unused - caller_config.get_dict("credential_manager", True) + options = builder.get_options() + self.options = options + self.logger = logger = user_sync.connector.helper.create_logger(options) + logger.debug('LDAP initialized with options: %s', options) self.user_identity_type = user_sync.identity_type.parse_identity_type(options['user_identity_type']) self.user_identity_type_formatter = LDAPValueFormatter(options['user_identity_type_format']) self.user_email_formatter = LDAPValueFormatter(options['user_email_format']) self.user_username_formatter = LDAPValueFormatter(options['user_username_format']) self.user_domain_formatter = LDAPValueFormatter(options['user_domain_format']) - - self.options = options - self.logger = logger = user_sync.connector.helper.create_logger(options) + + # sometimes the password is in plain text + cleartext_password = caller_config.get_string('password', True) + # sometimes the password is in the keyring + secure_password_key = caller_config.get_string('secure_password_key', True) + # but it has to be in one of those two places! + if not cleartext_password and not secure_password_key: + raise AssertionException('LDAP configuration must contain password or secure_password_key') + if cleartext_password and secure_password_key: + logger.warning('LDAP configuration specifies both secure_password_key and password; ' + 'preferring secure_password_key.') + if secure_password_key: + try: + password = keyring.get_password(service_name=secure_password_key, username=username) + except Exception as e: + raise AssertionException("Error accessing secure storage: %s" % e) + if password == None: + raise AssertionException('No value in secure storage for LDAP secure_password_key ' + '(' + secure_password_key + ')') + elif password == "": + logger.warning('Empty value in secure storage for LDAP secure_password_key (%s)', secure_password_key) + else: + password = cleartext_password + # this check must come after we get all the password values caller_config.report_unused_values(logger) - - require_tls_cert = options['require_tls_cert'] - logger.debug('Initialized with options: %s', options) logger.debug('Connecting to: %s using username: %s', host, username) - if not require_tls_cert: + if not options['require_tls_cert']: ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) - try: connection = ldap.initialize(host) connection.protocol_version = ldap.VERSION3 connection.set_option(ldap.OPT_REFERRALS, 0) connection.simple_bind_s(username, password) except Exception as e: - raise user_sync.error.AssertionException(repr(e)) + raise AssertionException('LDAP connection failure: ' + repr(e)) self.connection = connection logger.debug('Connected') @@ -159,20 +180,20 @@ def find_ldap_group(self, group, attribute_list=None): attrlist=attribute_list ) - group_tuple = None; + group_tuple = None for current_tuple in res: if (current_tuple[0] != None): if (group_tuple != None): - raise user_sync.error.AssertionException("Multiple LDAP groups found for: %s" % group) + raise AssertionException("Multiple LDAP groups found for: %s" % group) group_tuple = current_tuple return group_tuple def iter_attribute_values(self, dn, attribute_name, attributes=None): ''' - :type group_dn: str + :type dn: str :type attribute_name: str - :type group_attributes: dict(str, list) + :type attributes: dict(str, list) :rtype iterator ''' @@ -187,12 +208,12 @@ def iter_attribute_values(self, dn, attribute_name, attributes=None): result_type, result_response = connection.result(msgid) msgid = None if ((result_type == ldap.RES_SEARCH_RESULT or result_type == ldap.RES_SEARCH_ENTRY) and len(result_response) > 0): - current_tuple = result_response[0]; + current_tuple = result_response[0] if (current_tuple[0] != None): attributes = current_tuple[1] if (attributes == None): - break; + break for current_attribute_name, current_attribute_values in attributes.iteritems(): current_attribute_name_parts = current_attribute_name.split(';') @@ -200,11 +221,11 @@ def iter_attribute_values(self, dn, attribute_name, attributes=None): if (len(current_attribute_name_parts) > 1): upper_bound = self.get_range_upper_bound(current_attribute_name_parts[1]) if (upper_bound != None and upper_bound != '*'): - next_attribute_name = "%s;range=%s-*" % (attribute_name, str(int(upper_bound) + 1)); + next_attribute_name = "%s;range=%s-*" % (attribute_name, str(int(upper_bound) + 1)) msgid = connection.search(dn, ldap.SCOPE_BASE, attrlist=[next_attribute_name]) for current_attribute_value in current_attribute_values: try: - yield current_attribute_value; + yield current_attribute_value except GeneratorExit: if (msgid != None): connection.abandon(msgid) @@ -217,7 +238,7 @@ def get_range_upper_bound(self, range_statement): if (range_statement != None): statement_parts = range_statement.split('=') if (statement_parts[0] == 'range' and len(statement_parts) > 1): - range_parts = statement_parts[1].split('-'); + range_parts = statement_parts[1].split('-') if (len(range_parts) > 1): result = range_parts[1] return result @@ -232,7 +253,7 @@ def iter_ldap_group_members(self, group): if (group_tuple == None): self.logger.warning("No group found for: %s", group) else: - group_dn, group_attributes = group_tuple; + group_dn, group_attributes = group_tuple for attribute in attributes: attribute_values = self.iter_attribute_values(group_dn, attribute, group_attributes) for attribute_value in attribute_values: @@ -278,7 +299,7 @@ def iter_users(self, users_filter, extended_attributes): else: try: user['identity_type'] = user_sync.identity_type.parse_identity_type(identity_type) - except user_sync.error.AssertionException as e: + except AssertionException as e: self.logger.warning('Skipping user with dn %s: %s', dn, e.message) continue @@ -388,22 +409,20 @@ def get_attribute_names(self): def generate_value(self, record): ''' - :type parameter_names: list(str) :type record: dict - :type logger: logging :rtype (str, str) ''' result = None attribute_name = None - if (self.string_format != None): + if self.string_format is not None: values = {} for attribute_name in self.attribute_names: value = self.get_attribute_value(record, attribute_name) - if (value == None): + if value is None: values = None break values[attribute_name] = value - if (values != None): + if values is not None: result = self.string_format.format(**values) return (result, attribute_name) diff --git a/user_sync/connector/umapi.py b/user_sync/connector/umapi.py index 057c09c90..c481c3210 100644 --- a/user_sync/connector/umapi.py +++ b/user_sync/connector/umapi.py @@ -22,6 +22,7 @@ import logging import jwt +import keyring import umapi_client import helper @@ -55,35 +56,83 @@ def __init__(self, name, caller_options): server_builder.set_string_value('endpoint', '/v2/usermanagement') server_builder.set_string_value('ims_host', 'ims-na1.adobelogin.com') server_builder.set_string_value('ims_endpoint_jwt', '/ims/exchange/jwt') - options['server'] = server_options = server_builder.get_options() - + options['server'] = server_options = server_builder.get_options() + enterprise_config = caller_config.get_dict_config('enterprise') enterprise_builder = user_sync.config.OptionsBuilder(enterprise_config) enterprise_builder.require_string_value('org_id') enterprise_builder.require_string_value('api_key') - enterprise_builder.require_string_value('client_secret') enterprise_builder.require_string_value('tech_acct') - enterprise_builder.require_string_value('priv_key_path') options['enterprise'] = enterprise_options = enterprise_builder.get_options() - - self.options = options; + self.options = options self.logger = logger = helper.create_logger(options) - caller_config.report_unused_values(logger) - + server_config.report_unused_values(logger) + logger.debug('UMAPI initialized with options: %s', options) + + # set up the auth dict for umapi-client ims_host = server_options['ims_host'] self.org_id = org_id = enterprise_options['org_id'] - api_key = enterprise_options['api_key'] - private_key_file_path = enterprise_options['priv_key_path'] - um_endpoint = "https://" + server_options['host'] + server_options['endpoint'] - - logger.debug('Creating connection for org id: "%s" using private key file: "%s"', org_id, private_key_file_path) auth_dict = { "org_id": org_id, "tech_acct_id": enterprise_options['tech_acct'], - "api_key": api_key, - "client_secret": enterprise_options['client_secret'], - "private_key_file": private_key_file_path + "api_key": enterprise_options['api_key'], } + # get the client secret + plaintext_secret = enterprise_config.get_string('client_secret', True) + secure_secret_key = enterprise_config.get_string('secure_client_secret_key', True) + if not plaintext_secret and not secure_secret_key: + raise AssertionException('UMAPI configuration must contain client_secret or secure_client_secret_key') + if plaintext_secret and secure_secret_key: + logger.warning('UMAPI configuration contains both client_secret and secure_client_secret_key, ' + 'preferring secure_client_secret_key') + if secure_secret_key: + try: + secret = keyring.get_password(service_name=secure_secret_key, username=org_id) + except Exception as e: + raise AssertionException("Error accessing secure storage: %s" % e) + if secret == None: + raise AssertionException('No value in secure storage for UMAPI secure_client_secret_key ' + '(' + secure_secret_key + ')') + elif secret == "": + raise AssertionException('Empty value in secure storage for UMAPI secure_client_secret_key ' + '(' + secure_secret_key + ')') + auth_dict['client_secret'] = secret + else: + auth_dict['client_secret'] = plaintext_secret + # get the private key + plaintext_data_path = enterprise_config.get_string('priv_key_path', True) + plaintext_data = enterprise_config.get_string('priv_key_data', True) + secure_data_key = enterprise_config.get_string('secure_priv_key_data_key', True) + if plaintext_data_path: + if plaintext_data or secure_data_key: + raise AssertionException('UMAPI configuration has private key settings for both file and string data') + logger.debug("UMAPI reading private key data from file '%s'", plaintext_data_path) + auth_dict['private_key_file'] = plaintext_data_path + else: + if not plaintext_data and not secure_data_key: + raise AssertionException('UMAPI configuration must contain priv_key_data or secure_priv_key_data_key') + if plaintext_data and secure_data_key: + logger.warning('UMAPI configuration contains both priv_key_data and secure_priv_key_data_key, ' + 'preferring secure_priv_key_data_key') + if secure_data_key: + try: + data = keyring.get_password(service_name=secure_data_key, username=org_id) + except Exception as e: + raise AssertionException("Error accessing secure storage: %s" % e) + if data == None: + raise AssertionException('No value in secure storage for UMAPI secure_priv_key_data_key ' + '(' + secure_data_key + ')') + elif data == "": + raise AssertionException('Empty value in secure storage for UMAPI secure_priv_key_data_key ' + '(' + secure_data_key + ')') + auth_dict['private_key_data'] = data + else: + auth_dict['private_key_data'] = plaintext_data + # this check must come after we get all the secret and private_key values + enterprise_config.report_unused_values(logger) + # open the connection + um_endpoint = "https://" + server_options['host'] + server_options['endpoint'] + logger.debug('Creating connection for org %s at endpoint %s', org_id, um_endpoint) try: self.connection = connection = umapi_client.Connection( org_id=org_id, @@ -96,10 +145,9 @@ def __init__(self, name, caller_options): logger=self.logger, ) except Exception as e: - raise AssertionException("UMAPI connection to org id '%s' failed: %s" % (org_id, e)) - - logger.debug('API initialized on: %s', um_endpoint) - + raise AssertionException("UMAPI connection to org %s at endpoint %s failed: %s" % (org_id, um_endpoint, e)) + logger.debug('UMAPI connection established') + # wrap the connection in an action manager self.action_manager = ActionManager(connection, org_id, logger) def get_users(self): @@ -134,8 +182,8 @@ def __init__(self, identity_type = None, email = None, username = None, domain = :type username: str :type domain: str ''' - self.identity_type = identity_type; - self.email = email; + self.identity_type = identity_type + self.email = email self.username = username self.domain = domain self.do_list = [] diff --git a/user_sync/credential_manager.py b/user_sync/credential_manager.py deleted file mode 100755 index 0065947c0..000000000 --- a/user_sync/credential_manager.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) 2016-2017 Adobe Systems Incorporated. All rights reserved. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -import keyring - -from user_sync.error import AssertionException - -UMAPI_CREDENTIAL_TYPE = "Adobe_UMAPI" -DIRECTORY_CREDENTIAL_TYPE = "Directory" -KEYRING_SUPPORTED = ['windows_credential_manager'] - -def get_credentials(credential_type, credential_id, **kwArgs): - ''' - This allows a customer to customize how credentials are fetched. - For the UMAPI related credential: - the application will specify credential_type as "Adobe_UMAPI" and credential_id is the org_id - For directory related credential such as LDAP - the credential_type would be "Directory" and the credential_id would be the connector protocol (ldap, etc.) - kwArgs is a dictionary containing extra data, with: - - 'config' being the current config for the credential to retrieve. - - 'config_loader' being the instance of the loader - - The application supports these return types: - - config object in dict - - file path to a yaml file containing a config object - - None - - :type credential_type: str - :type id: str - :type kwArgs: dict - :rtype dict | str | None - ''' - if credential_type == DIRECTORY_CREDENTIAL_TYPE: - config = kwArgs['config'] - if 'credential_manager' in config: - cred_man = config['credential_manager'] - if (cred_man.get('type') in KEYRING_SUPPORTED and - 'service_name' in cred_man and 'username' in config): - return get_credentials_from_keyring(cred_man['service_name'], config['username']) - return None - -def get_credentials_from_keyring(service_name, username): - try: - cred = keyring.get_password(service_name=service_name, username=username) - except Exception as e: - raise AssertionException("Error accessing credential manager: %s" % e) - if cred == None: - raise AssertionException("Unable to retrieve password for service_name: '%s' service_username: %s" % (service_name, username)) - elif cred == "": - raise AssertionException ("Password is empty for service_name: '%s' service_username: %s" % (service_name, username)) - return {'username': username, 'password': cred}