diff --git a/examples/config files - basic/3 connector-ldap.yml b/examples/config files - basic/3 connector-ldap.yml old mode 100644 new mode 100755 index 1229b7bc9..d63c53505 --- a/examples/config files - basic/3 connector-ldap.yml +++ b/examples/config files - basic/3 connector-ldap.yml @@ -17,11 +17,19 @@ # You must specify all four of these settings. Consult with your # enterprise directory administrators to get suitable values. # These access credentials are sensitive and must be protected. -username: "LDAP username goes here" +username: "LDAP or Credential Manager username goes here" password: "LDAP password goes here" host: "LDAP host URL goes here. e.g. ldap://ldap.example.com" base_dn: "defines the base DN. e.g. DC=example,DC=com" +#(optional) +credential_manager: + #This will pull credential from Windows Credential Manager in Control Panel. + #value: windows_credential_manager + type: windows_credential_manager + #service_name is Required for Windows Credential Manger + service_name: "Internet or Network Address field in Credential Manager" + # (optional) user_identity_type (default is inherited from main configuration) # user_identity_type specifies a default identity type for when directory users # are created on the Adobe side (one of adobeID, enterpriseID, federatedID). diff --git a/misc/build/requirements.txt b/misc/build/requirements.txt index 3d2acd530..12a33646d 100644 --- a/misc/build/requirements.txt +++ b/misc/build/requirements.txt @@ -2,3 +2,4 @@ pycrypto PyYAML psutil umapi-client>=2.0.2 +keyring diff --git a/setup.py b/setup.py index d89716c73..d790c7f7f 100644 --- a/setup.py +++ b/setup.py @@ -43,8 +43,9 @@ 'pycrypto', 'python-ldap==2.4.25', 'PyYAML', - 'umapi-client>=2.2', + 'umapi-client>=2.3', 'psutil', + 'keyring' ], setup_requires=['nose>=1.0'], tests_require=[ diff --git a/user_sync/config.py b/user_sync/config.py index a9d290078..c3b693672 100644 --- a/user_sync/config.py +++ b/user_sync/config.py @@ -21,13 +21,13 @@ import logging import os import re - import types + +import keyring import yaml 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 +139,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 +154,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 +205,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 +238,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,26 +346,12 @@ 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): directory_connectors_config = self.get_directory_connector_configs() self.main_config.report_unused_values(self.logger, [directory_connectors_config]) - class ObjectConfig(object): def __init__(self, scope): ''' @@ -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,10 +541,66 @@ 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 - + + keyring_prefix = 'secure_' + keyring_suffix = '_key' + + def has_credential(self, name): + ''' + Check if there is a credential setting with the given name + :param name: plaintext setting name for the credential + :return: setting that was specified, or None if none was + ''' + scope = self.get_full_scope() + keyring_name = self.keyring_prefix + name + self.keyring_suffix + plaintext = self.get_string(name, True) + secure = self.get_string(keyring_name, True) + if plaintext and secure: + raise AssertionException('%s: cannot contain setting for both "%s" and "%s"' % (scope, name, keyring_name)) + if plaintext is not None: + return name + elif secure is not None: + return keyring_name + else: + return None + + def get_credential(self, name, user_name, none_allowed=False): + ''' + Get the credential with the given name. Raises an AssertionException if there + is no credential, or if the credential is specified both in plaintext and the keyring. + If the credential is kept in the keyring, the value of the keyring_name setting + gives the secure storage key, and we fetch that key for the given user. + :param name: setting name for the plaintext credential + :param user_name: the user for whom we should fetch the service name password in secure storage + :param none_allowed: whether the credential can be missing or empty + :return: credential string + ''' + keyring_name = self.keyring_prefix + name + self.keyring_suffix + scope = self.get_full_scope() + # sometimes the credential is in plain text + cleartext_value = self.get_string(name, True) + # sometimes the value is in the keyring + secure_value_key = self.get_string(keyring_name, True) + # but it has to be in exactly one of those two places! + if not cleartext_value and not secure_value_key and not none_allowed: + raise AssertionException('%s: must contain setting for "%s" or "%s"' % (scope, name, keyring_name)) + if cleartext_value and secure_value_key: + raise AssertionException('%s: cannot contain setting for both "%s" and "%s"' % (scope, name, keyring_name)) + if secure_value_key: + try: + value = keyring.get_password(service_name=secure_value_key, username=user_name) + except Exception as e: + raise AssertionException('%s: Error accessing secure storage: %s' % (scope, e)) + else: + value = cleartext_value + if not value and not none_allowed: + raise AssertionException( + '%s: No value in secure storage for user "%s", key "%s"' % (scope, user_name, secure_value_key)) + return value + class ConfigFileLoader: ''' Loads config files and does pathname expansion on settings that refer to files or directories @@ -717,8 +749,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 +826,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 +842,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..1f2804479 100644 --- a/user_sync/connector/directory_csv.py +++ b/user_sync/connector/directory_csv.py @@ -53,7 +53,7 @@ class CSVDirectoryConnector(object): name = 'csv' def __init__(self, caller_options): - caller_config = user_sync.config.DictConfig('"%s options"' % CSVDirectoryConnector.name, caller_options) + caller_config = user_sync.config.DictConfig('%s configuration' % self.name, caller_options) builder = user_sync.config.OptionsBuilder(caller_config) builder.set_string_value('delimiter', None) builder.set_string_value('first_name_column_name', 'firstname') @@ -65,18 +65,16 @@ def __init__(self, caller_options): builder.set_string_value('domain_column_name', 'domain') builder.set_string_value('identity_type_column_name', 'type') builder.set_string_value('user_identity_type', None) - builder.set_string_value('logger_name', CSVDirectoryConnector.name) + builder.set_string_value('logger_name', self.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('%s initialized with options: %s', self.name, 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 old mode 100644 new mode 100755 index 8f4ec308f..219ba7db6 --- 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 = { @@ -55,7 +59,7 @@ class LDAPDirectoryConnector(object): group_member_attribute = "member" def __init__(self, caller_options): - caller_config = user_sync.config.DictConfig('"%s options"' % LDAPDirectoryConnector.name, caller_options) + caller_config = user_sync.config.DictConfig('%s configuration' % self.name, caller_options) builder = user_sync.config.OptionsBuilder(caller_config) builder.set_string_value('group_filter_format', '(&(|(objectCategory=group)(objectClass=groupOfNames)(objectClass=posixGroup))(cn={group}))') builder.set_string_value('all_users_filter', '(&(objectClass=user)(objectCategory=person)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))') @@ -70,33 +74,31 @@ 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') + options = builder.get_options() + self.options = options + self.logger = logger = user_sync.connector.helper.create_logger(options) + logger.debug('%s initialized with options: %s', self.name, 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) + + password = caller_config.get_credential('password', options['username']) + # this check must come after we get the password value 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') @@ -157,20 +159,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 ''' @@ -185,12 +187,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(';') @@ -198,11 +200,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) @@ -215,7 +217,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 @@ -230,7 +232,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: @@ -276,7 +278,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 @@ -386,22 +388,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..828f51404 100644 --- a/user_sync/connector/umapi.py +++ b/user_sync/connector/umapi.py @@ -43,9 +43,10 @@ def __init__(self, name, caller_options): :type name: str :type caller_options: dict ''' - caller_config = user_sync.config.DictConfig('"%s umapi options"' % name, caller_options) + self.name = 'umapi' + name + caller_config = user_sync.config.DictConfig(self.name + ' configuration', caller_options) builder = user_sync.config.OptionsBuilder(caller_config) - builder.set_string_value('logger_name', 'umapi' + name) + builder.set_string_value('logger_name', self.name) builder.set_bool_value('test_mode', False) options = builder.get_options() @@ -55,35 +56,43 @@ 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 + 'org_id': org_id, + 'tech_acct_id': enterprise_options['tech_acct'], + 'api_key': enterprise_config.get_credential('api_key', org_id), + 'client_secret': enterprise_config.get_credential('client_secret', org_id), } + # get the private key + key_path = enterprise_config.get_string('priv_key_path', True) + if key_path: + data_setting = enterprise_config.has_credential('priv_key_data') + if data_setting: + raise AssertionException('%s: cannot specify both "priv_key_path" and "%s"' % + (enterprise_config.get_full_scope(), data_setting)) + logger.debug('%s: reading private key data from file %s', self.name, key_path) + auth_dict['private_key_file'] = key_path + else: + auth_dict['private_key_data'] = enterprise_config.get_credential('priv_key_data', org_id) + # this check must come after we fetch all the settings + enterprise_config.report_unused_values(logger) + # open the connection + um_endpoint = "https://" + server_options['host'] + server_options['endpoint'] + logger.debug('%s: creating connection for org %s at endpoint %s', self.name, org_id, um_endpoint) try: self.connection = connection = umapi_client.Connection( org_id=org_id, @@ -96,10 +105,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("Connection to org %s at endpoint %s failed: %s" % (org_id, um_endpoint, e)) + logger.debug('%s: connection established', self.name) + # wrap the connection in an action manager self.action_manager = ActionManager(connection, org_id, logger) def get_users(self): @@ -134,8 +142,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 = [] @@ -162,7 +170,7 @@ def add_groups(self, groups_to_add): self.do_list.append(('add_to_groups', params)) def remove_all_groups(self): - self.do_list.append(('remove_from_groups', "all")) + self.do_list.append(('remove_from_groups', 'all')) def remove_groups(self, groups_to_remove): ''' @@ -170,16 +178,10 @@ def remove_groups(self, groups_to_remove): ''' if (groups_to_remove != None and len(groups_to_remove) > 0): params = { - "groups": groups_to_remove + 'groups': groups_to_remove } self.do_list.append(('remove_from_groups', params)) - def remove_all_groups(self): - params = { - "all_groups": True - } - self.do_list.append(('remove_from_groups', params)) - def add_user(self, attributes): ''' :type attributes: dict diff --git a/user_sync/credential_manager.py b/user_sync/credential_manager.py deleted file mode 100644 index e3e68fe4c..000000000 --- a/user_sync/credential_manager.py +++ /dev/null @@ -1,46 +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. - -UMAPI_CREDENTIAL_TYPE = "Adobe_UMAPI" -DIRECTORY_CREDENTIAL_TYPE = "Directory" - -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 - ''' - return None - diff --git a/user_sync/rules.py b/user_sync/rules.py index 2fafa450c..ef4b886f9 100644 --- a/user_sync/rules.py +++ b/user_sync/rules.py @@ -123,10 +123,10 @@ def __init__(self, caller_options): 'hook_storage': None, # for exclusive use by hook code; persists across calls } - if (logger.isEnabledFor(logging.DEBUG)): + if logger.isEnabledFor(logging.DEBUG): options_to_report = options.copy() username_filter_regex = options_to_report['username_filter_regex'] - if (username_filter_regex != None): + if username_filter_regex is not None: options_to_report['username_filter_regex'] = "%s: %s" % (type(username_filter_regex), username_filter_regex.pattern) logger.debug('Initialized with options: %s', options_to_report) @@ -140,7 +140,7 @@ def run(self, directory_groups, directory_connector, umapi_connectors): self.prepare_umapi_infos() - if (directory_connector != None): + if directory_connector is not None: load_directory_stats = user_sync.helper.JobStats("Load from Directory", divider = "-") load_directory_stats.log_start(logger) self.read_desired_user_groups(directory_groups, directory_connector) @@ -250,7 +250,7 @@ def will_manage_groups(self): def get_umapi_info(self, umapi_name): umapi_info = self.umapi_info_by_name.get(umapi_name) - if (umapi_info == None): + if umapi_info is None: self.umapi_info_by_name[umapi_name] = umapi_info = UmapiTargetInfo(umapi_name) return umapi_info @@ -271,7 +271,7 @@ def read_desired_user_groups(self, mappings, directory_connector): options = self.options directory_group_filter = options['directory_group_filter'] - if (directory_group_filter != None): + if directory_group_filter is not None: directory_group_filter = set(directory_group_filter) extended_attributes = options.get('extended_attributes') @@ -279,7 +279,7 @@ def read_desired_user_groups(self, mappings, directory_connector): filtered_directory_user_by_user_key = self.filtered_directory_user_by_user_key directory_groups = set(mappings.iterkeys()) - if (directory_group_filter != None): + if directory_group_filter is not None: directory_groups.update(directory_group_filter) directory_users = directory_connector.load_users_and_groups(directory_groups, extended_attributes) @@ -329,14 +329,14 @@ def read_desired_user_groups(self, mappings, directory_connector): for target_group_qualified_name in self.after_mapping_hook_scope['target_groups']: target_group = AdobeGroup.lookup(target_group_qualified_name) - if (target_group is not None): + if target_group is not None: umapi_info = self.get_umapi_info(target_group.get_umapi_name()) umapi_info.add_desired_group_for(user_key, target_group.get_group_name()) else: self.logger.error('Target adobe group %s is not known; ignored', target_group_qualified_name) self.logger.debug('Total directory users after filtering: %d', len(filtered_directory_user_by_user_key)) - if (self.logger.isEnabledFor(logging.DEBUG)): + if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug('Group work list: %s', dict([(umapi_name, umapi_info.get_desired_groups_by_user_key()) for umapi_name, umapi_info in self.umapi_info_by_name.iteritems()])) @@ -350,7 +350,7 @@ def is_directory_user_in_groups(self, directory_user, groups): if groups == None: return True for directory_user_group in directory_user['groups']: - if (directory_user_group in groups): + if directory_user_group in groups: return True return False @@ -388,7 +388,7 @@ def process_umapi_users(self, umapi_connectors): # Now manage the adobe groups in the secondaries for umapi_name, umapi_connector in umapi_connectors.get_secondary_connectors().iteritems(): secondary_umapi_info = self.get_umapi_info(umapi_name) - if (len(secondary_umapi_info.get_mapped_groups()) == 0): + if len(secondary_umapi_info.get_mapped_groups()) == 0: continue self.logger.debug('Syncing users to secondary umapi %s...', umapi_name) secondary_updates_by_user_key = self.update_umapi_users_for_connector(secondary_umapi_info, umapi_connector) @@ -401,10 +401,10 @@ def is_selected_user_key(self, user_key): :type user_key: str ''' username_filter_regex = self.options['username_filter_regex'] - if (username_filter_regex != None): + if username_filter_regex is not None: username = self.get_username_from_user_key(user_key) search_result = username_filter_regex.search(username) - if (search_result == None): + if search_result is None: return False return True @@ -541,25 +541,24 @@ def get_user_attributes(self, directory_user): def get_identity_type_from_directory_user(self, directory_user): identity_type = directory_user.get('identity_type') - if (identity_type == None): + if identity_type is None: identity_type = self.options['new_account_type'] self.logger.warning('Found user with no identity type, using %s: %s', identity_type, directory_user) return identity_type def get_identity_type_from_umapi_user(self, umapi_user): identity_type = umapi_user.get('type') - if (identity_type == None): + if identity_type is None: identity_type = self.options['new_account_type'] self.logger.error('Found adobe user with no identity type, using %s: %s', identity_type, umapi_user) return identity_type def create_commands_from_directory_user(self, directory_user, identity_type = None): ''' - :type user_key: str - :type identity_type: str :type directory_user: dict + :type identity_type: str ''' - if (identity_type == None): + if identity_type is None: identity_type = self.get_identity_type_from_directory_user(directory_user) commands = user_sync.connector.umapi.Commands(identity_type, directory_user['email'], directory_user['username'], directory_user['domain']) @@ -595,9 +594,9 @@ def add_umapi_user(self, user_key, groups_to_add, umapi_connectors): " and no default has been specified.", user_key) return attributes['country'] = country - if (attributes.get('firstname') == None): + if attributes.get('firstname') is None: attributes.pop('firstname', None) - if (attributes.get('lastname') == None): + if attributes.get('lastname') is None: attributes.pop('lastname', None) attributes['option'] = "updateIfAlreadyExists" if update_user_info else 'ignoreIfAlreadyExists' @@ -605,7 +604,7 @@ def add_umapi_user(self, user_key, groups_to_add, umapi_connectors): self.logger.info('Adding directory user with user key: %s', user_key) self.action_summary['adobe_users_created'] += 1 primary_commands.add_user(attributes) - if (manage_groups): + if manage_groups: primary_commands.add_groups(groups_to_add) umapi_connectors.get_primary_connector().send_commands(primary_commands) # add the user to secondaries without groups @@ -726,6 +725,7 @@ def update_umapi_users_for_connector(self, umapi_info, umapi_connector): # for removal from any mapped groups. if exclude_strays: self.logger.debug("Excluding Adobe-only user: %s", user_key) + self.excluded_user_count += 1 elif will_process_strays: self.logger.debug("Found Adobe-only user: %s", user_key) self.add_stray(umapi_info.get_name(), user_key, @@ -781,7 +781,7 @@ def normalize_groups(group_names): :rtype set(str) ''' result = set() - if (group_names != None): + if group_names is not None: for group_name in group_names: normalized_group_name = user_sync.helper.normalize_string(group_name) result.add(normalized_group_name) @@ -809,9 +809,9 @@ def calculate_groups_to_remove(self, umapi_info, user_key, desired_groups): :type desired_groups: set(str) ''' groups_to_remove = self.get_new_groups(umapi_info.groups_removed_by_user_key, user_key, desired_groups) - if (desired_groups != None and self.logger.isEnabledFor(logging.DEBUG)): + if desired_groups is not None and self.logger.isEnabledFor(logging.DEBUG): groups_already_removed = desired_groups - groups_to_remove - if (len(groups_already_removed) > 0): + if len(groups_already_removed) > 0: self.logger.debug('Skipped removed groups for user: %s groups: %s', user_key, groups_already_removed) return groups_to_remove @@ -823,14 +823,14 @@ def get_new_groups(self, current_groups_by_user_key, user_key, desired_groups): :type desired_groups: set(str) ''' new_groups = None - if (desired_groups != None): + if desired_groups is not None: current_groups = current_groups_by_user_key.get(user_key) - if (current_groups != None): + if current_groups is not None: new_groups = desired_groups - current_groups else: new_groups = desired_groups - if (len(new_groups) > 0): - if (current_groups == None): + if len(new_groups) > 0: + if current_groups is None: current_groups_by_user_key[user_key] = current_groups = set() current_groups |= new_groups return new_groups @@ -841,7 +841,7 @@ def get_user_attribute_difference(self, directory_user, umapi_user): attributes = self.get_user_attributes(directory_user) for key, value in attributes.iteritems(): umapi_value = umapi_user.get(key) - if (value != umapi_value): + if value != umapi_value: differences[key] = value return differences @@ -882,7 +882,7 @@ def get_user_key(self, id_type, username, domain, email=None): return None if not username: return None - if (username.find('@') >= 0): + if username.find('@') >= 0: domain = "" elif not domain: return None @@ -903,7 +903,6 @@ def read_stray_key_map(self, file_path, delimiter = None): Load the users to be removed from a CSV file. Returns the stray key map. :type file_path: str :type delimiter: str - :type logger: logging.Logger ''' self.logger.info('Reading Adobe-only users from: %s', file_path) id_type_column_name = 'type' @@ -975,16 +974,16 @@ def write_stray_key_map(self): logger.info('Wrote %d Adobe-only user%s.', user_count, user_plural) def log_after_mapping_hook_scope(self, before_call=None, after_call=None): - if ((before_call is None and after_call is None) or (before_call is not None and after_call is not None)): + if (before_call is None and after_call is None) or (before_call is not None and after_call is not None): raise ValueError("Exactly one of 'before_call', 'after_call' must be passed (and not None)") when = 'before' if before_call is not None else 'after' - if (before_call is not None): + if before_call is not None: self.logger.debug('.') self.logger.debug('Source attrs, %s: %s', when, self.after_mapping_hook_scope['source_attributes']) self.logger.debug('Source groups, %s: %s', when, self.after_mapping_hook_scope['source_groups']) self.logger.debug('Target attrs, %s: %s', when, self.after_mapping_hook_scope['target_attributes']) self.logger.debug('Target groups, %s: %s', when, self.after_mapping_hook_scope['target_groups']) - if (after_call is not None): + if after_call is not None: self.logger.debug('Hook storage, %s: %s', when, self.after_mapping_hook_scope['hook_storage']) @@ -1046,7 +1045,7 @@ def __str__(self): def get_qualified_name(self): prefix = "" - if (self.umapi_name is not None and self.umapi_name != PRIMARY_UMAPI_NAME): + if self.umapi_name is not None and self.umapi_name != PRIMARY_UMAPI_NAME: prefix = self.umapi_name + GROUP_NAME_DELIMITER return prefix + self.group_name @@ -1065,7 +1064,7 @@ def _parse(qualified_name): parts = qualified_name.split(GROUP_NAME_DELIMITER) group_name = parts.pop() umapi_name = GROUP_NAME_DELIMITER.join(parts) - if (len(umapi_name) == 0): + if len(umapi_name) == 0: umapi_name = PRIMARY_UMAPI_NAME return group_name, umapi_name @@ -1132,9 +1131,9 @@ def add_desired_group_for(self, user_key, group): :type group: str ''' desired_groups = self.get_desired_groups(user_key) - if (desired_groups == None): + if desired_groups is None: self.desired_groups_by_user_key[user_key] = desired_groups = set() - if (group != None): + if group is not None: normalized_group_name = user_sync.helper.normalize_string(group) desired_groups.add(normalized_group_name)