Skip to content

Commit a5d88f2

Browse files
authored
Merge pull request #172 from adobe-apiplatform/issue-159
intermediate work on #159. Fix #173.
2 parents 43df910 + 44238fd commit a5d88f2

File tree

9 files changed

+208
-212
lines changed

9 files changed

+208
-212
lines changed

examples/config files - basic/3 connector-ldap.yml

100644100755
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,19 @@
1717
# You must specify all four of these settings. Consult with your
1818
# enterprise directory administrators to get suitable values.
1919
# These access credentials are sensitive and must be protected.
20-
username: "LDAP username goes here"
20+
username: "LDAP or Credential Manager username goes here"
2121
password: "LDAP password goes here"
2222
host: "LDAP host URL goes here. e.g. ldap://ldap.example.com"
2323
base_dn: "defines the base DN. e.g. DC=example,DC=com"
2424

25+
#(optional)
26+
credential_manager:
27+
#This will pull credential from Windows Credential Manager in Control Panel.
28+
#value: windows_credential_manager
29+
type: windows_credential_manager
30+
#service_name is Required for Windows Credential Manger
31+
service_name: "Internet or Network Address field in Credential Manager"
32+
2533
# (optional) user_identity_type (default is inherited from main configuration)
2634
# user_identity_type specifies a default identity type for when directory users
2735
# are created on the Adobe side (one of adobeID, enterpriseID, federatedID).

misc/build/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ pycrypto
22
PyYAML
33
psutil
44
umapi-client>=2.0.2
5+
keyring

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@
4343
'pycrypto',
4444
'python-ldap==2.4.25',
4545
'PyYAML',
46-
'umapi-client>=2.2',
46+
'umapi-client>=2.3',
4747
'psutil',
48+
'keyring'
4849
],
4950
setup_requires=['nose>=1.0'],
5051
tests_require=[

user_sync/config.py

Lines changed: 87 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
import logging
2222
import os
2323
import re
24-
2524
import types
25+
26+
import keyring
2627
import yaml
2728

2829
import user_sync.identity_type
2930
import user_sync.rules
30-
from user_sync import credential_manager
3131
from user_sync.error import AssertionException
3232

3333
DEFAULT_MAIN_CONFIG_FILENAME = 'user-sync-config.yml'
@@ -139,20 +139,10 @@ def get_directory_connector_options(self, connector_name):
139139
'''
140140
options = {}
141141
connectors_config = self.get_directory_connector_configs()
142-
if (connectors_config != None):
142+
if connectors_config is not None:
143143
connector_item = connectors_config.get_list(connector_name, True)
144144
options = self.get_dict_from_sources(connector_item)
145-
146145
options = self.combine_dicts([options, self.options['directory_connector_overridden_options']])
147-
# credentials are None, a dict, or a config filename to read to get a dict
148-
credentials = credential_manager.get_credentials(credential_manager.DIRECTORY_CREDENTIAL_TYPE,
149-
connector_name,
150-
config=options,
151-
config_loader = self)
152-
if isinstance(credentials, types.StringTypes):
153-
credentials = ConfigFileLoader.load_other_config(credentials)
154-
if isinstance(credentials, dict):
155-
options = self.combine_dicts([options, credentials])
156146
return options
157147

158148
def get_directory_groups(self):
@@ -164,9 +154,9 @@ def get_directory_groups(self):
164154
raise AssertionException("Your main configuration file is still in v1 format. Please convert it to v2.")
165155
groups_config = None
166156
directory_config = self.main_config.get_dict_config('directory_users', True)
167-
if (directory_config != None):
157+
if directory_config is not None:
168158
groups_config = directory_config.get_list_config('groups', True)
169-
if (groups_config == None):
159+
if groups_config is None:
170160
return adobe_groups_by_directory_group
171161

172162
for item in groups_config.iter_dict_configs():
@@ -215,7 +205,6 @@ def get_dict_from_sources(self, sources):
215205
Given a list of config file paths, return the dictionary composed of all the contents
216206
of those config files, or None if the list is empty
217207
:param sources: a list of strings
218-
:param owner: a string to use in error messages if we can't find a config file.
219208
:rtype dict
220209
'''
221210
if not sources:
@@ -249,10 +238,10 @@ def combine_dicts(dicts):
249238
'''
250239
result = {}
251240
for dict_item in dicts:
252-
if (isinstance(dict_item, dict)):
241+
if isinstance(dict_item, dict):
253242
for dict_key, dict_item in dict_item.iteritems():
254243
result_item = result.get(dict_key)
255-
if (isinstance(result_item, dict) and isinstance(dict_item, dict)):
244+
if isinstance(result_item, dict) and isinstance(dict_item, dict):
256245
result_item.update(dict_item)
257246
else:
258247
result[dict_key] = dict_item
@@ -357,26 +346,12 @@ def get_rule_options(self):
357346
def create_umapi_options(self, connector_config_sources):
358347
options = self.get_dict_from_sources(connector_config_sources)
359348
options['test_mode'] = self.options['test_mode']
360-
enterprise_section = options.get('enterprise')
361-
if isinstance(enterprise_section, dict):
362-
org_id = enterprise_section.get('org_id')
363-
if (org_id != None):
364-
# credentials are None, a dict, or a config filename to read to get a dict
365-
credentials = credential_manager.get_credentials(credential_manager.UMAPI_CREDENTIAL_TYPE,
366-
org_id,
367-
config = enterprise_section,
368-
config_loader = self)
369-
if isinstance(credentials, types.StringTypes):
370-
credentials = ConfigFileLoader.load_other_config(credentials)
371-
if isinstance(credentials, dict):
372-
options['enterprise'] = self.combine_dicts([enterprise_section, credentials])
373349
return options
374350

375351
def check_unused_config_keys(self):
376352
directory_connectors_config = self.get_directory_connector_configs()
377353
self.main_config.report_unused_values(self.logger, [directory_connectors_config])
378354

379-
380355
class ObjectConfig(object):
381356
def __init__(self, scope):
382357
'''
@@ -420,30 +395,31 @@ def create_assertion_error(self, message):
420395
return AssertionException("%s in: %s" % (message, self.get_full_scope()))
421396

422397
def describe_types(self, types_to_describe):
423-
if (types_to_describe == types.StringTypes):
398+
if types_to_describe == types.StringTypes:
424399
result = self.describe_types(types.StringType)
425-
elif (isinstance(types_to_describe, tuple)):
400+
elif isinstance(types_to_describe, tuple):
426401
result = []
427402
for type_to_describe in types_to_describe:
428403
result.extend(self.describe_types(type_to_describe))
429404
else:
430405
result = [types_to_describe.__name__]
431406
return result
432407

433-
def report_unused_values(self, logger, optional_configs = []):
408+
def report_unused_values(self, logger, optional_configs=None):
409+
optional_configs = [] if optional_configs is None else optional_configs
434410
has_error = False
435411
for config in self.iter_configs():
436412
messages = config.describe_unused_values()
437-
if (len(messages) > 0):
438-
if (config in optional_configs):
413+
if len(messages) > 0:
414+
if config in optional_configs:
439415
log_level = logging.WARNING
440416
else:
441417
log_level = logging.ERROR
442418
has_error = True
443419
for message in messages:
444420
logger.log(log_level, message)
445421

446-
if (has_error):
422+
if has_error:
447423
raise AssertionException('Detected unused keys that are not ignorable.')
448424

449425
def describe_unused_values(self):
@@ -465,7 +441,7 @@ def iter_values(self, allowed_types):
465441
'''
466442
index = 0
467443
for item in self.value:
468-
if (not isinstance(item, allowed_types)):
444+
if not isinstance(item, allowed_types):
469445
reported_types = self.describe_types(allowed_types)
470446
raise self.create_assertion_error("Value should be one of these types: %s for index: %s" % (reported_types, index))
471447
index += 1
@@ -475,7 +451,7 @@ def iter_dict_configs(self):
475451
index = 0
476452
for value in self.iter_values(dict):
477453
config = self.find_child_config(index)
478-
if (config == None):
454+
if config is None:
479455
config = DictConfig("[%s]" % index, value)
480456
self.add_child(config)
481457
yield config
@@ -500,17 +476,17 @@ def iter_keys(self):
500476

501477
def iter_unused_keys(self):
502478
for key in self.iter_keys():
503-
if (key not in self.accessed_keys):
479+
if key not in self.accessed_keys:
504480
yield key
505481

506482
def get_dict_config(self, key, none_allowed = False):
507483
'''
508484
:rtype DictConfig
509485
'''
510486
result = self.find_child_config(key)
511-
if (result == None):
487+
if result is None:
512488
value = self.get_dict(key, none_allowed)
513-
if (value != None):
489+
if value is not None:
514490
result = DictConfig(key, value)
515491
self.add_child(result)
516492
return result
@@ -530,7 +506,7 @@ def get_bool(self, key, none_allowed = False):
530506

531507
def get_list(self, key, none_allowed = False):
532508
value = self.get_value(key, None, none_allowed)
533-
if (value != None and not isinstance(value, list)):
509+
if value is not None and not isinstance(value, list):
534510
value = [value]
535511
return value
536512

@@ -539,9 +515,9 @@ def get_list_config(self, key, none_allowed = False):
539515
:rtype ListConfig
540516
'''
541517
result = self.find_child_config(key)
542-
if (result == None):
518+
if result is None:
543519
value = self.get_list(key, none_allowed)
544-
if (value != None):
520+
if value is not None:
545521
result = ListConfig(key, value)
546522
self.add_child(result)
547523
return result
@@ -554,21 +530,77 @@ def get_value(self, key, allowed_types, none_allowed = False):
554530
'''
555531
self.accessed_keys.add(key)
556532
result = self.value.get(key)
557-
if (result == None):
558-
if (not none_allowed):
533+
if result is None:
534+
if not none_allowed:
559535
raise self.create_assertion_error("Value not found for key: %s" % key)
560-
elif (allowed_types != None and not isinstance(result, allowed_types)):
536+
elif allowed_types is not None and not isinstance(result, allowed_types):
561537
reported_types = self.describe_types(allowed_types)
562538
raise self.create_assertion_error("Value should be one of these types: %s for key: %s" % (reported_types, key))
563539
return result
564540

565541
def describe_unused_values(self):
566542
messages = []
567543
unused_keys = list(self.iter_unused_keys())
568-
if (len(unused_keys) > 0):
544+
if len(unused_keys) > 0:
569545
messages.append("Found unused keys: %s in: %s" % (unused_keys, self.get_full_scope()))
570546
return messages
571-
547+
548+
keyring_prefix = 'secure_'
549+
keyring_suffix = '_key'
550+
551+
def has_credential(self, name):
552+
'''
553+
Check if there is a credential setting with the given name
554+
:param name: plaintext setting name for the credential
555+
:return: setting that was specified, or None if none was
556+
'''
557+
scope = self.get_full_scope()
558+
keyring_name = self.keyring_prefix + name + self.keyring_suffix
559+
plaintext = self.get_string(name, True)
560+
secure = self.get_string(keyring_name, True)
561+
if plaintext and secure:
562+
raise AssertionException('%s: cannot contain setting for both "%s" and "%s"' % (scope, name, keyring_name))
563+
if plaintext is not None:
564+
return name
565+
elif secure is not None:
566+
return keyring_name
567+
else:
568+
return None
569+
570+
def get_credential(self, name, user_name, none_allowed=False):
571+
'''
572+
Get the credential with the given name. Raises an AssertionException if there
573+
is no credential, or if the credential is specified both in plaintext and the keyring.
574+
If the credential is kept in the keyring, the value of the keyring_name setting
575+
gives the secure storage key, and we fetch that key for the given user.
576+
:param name: setting name for the plaintext credential
577+
:param user_name: the user for whom we should fetch the service name password in secure storage
578+
:param none_allowed: whether the credential can be missing or empty
579+
:return: credential string
580+
'''
581+
keyring_name = self.keyring_prefix + name + self.keyring_suffix
582+
scope = self.get_full_scope()
583+
# sometimes the credential is in plain text
584+
cleartext_value = self.get_string(name, True)
585+
# sometimes the value is in the keyring
586+
secure_value_key = self.get_string(keyring_name, True)
587+
# but it has to be in exactly one of those two places!
588+
if not cleartext_value and not secure_value_key and not none_allowed:
589+
raise AssertionException('%s: must contain setting for "%s" or "%s"' % (scope, name, keyring_name))
590+
if cleartext_value and secure_value_key:
591+
raise AssertionException('%s: cannot contain setting for both "%s" and "%s"' % (scope, name, keyring_name))
592+
if secure_value_key:
593+
try:
594+
value = keyring.get_password(service_name=secure_value_key, username=user_name)
595+
except Exception as e:
596+
raise AssertionException('%s: Error accessing secure storage: %s' % (scope, e))
597+
else:
598+
value = cleartext_value
599+
if not value and not none_allowed:
600+
raise AssertionException(
601+
'%s: No value in secure storage for user "%s", key "%s"' % (scope, user_name, secure_value_key))
602+
return value
603+
572604
class ConfigFileLoader:
573605
'''
574606
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):
717749
does the relative path processing for a value from the dictionary,
718750
which can be a string, a list of strings, or a list of strings
719751
and "tagged" strings (sub-dictionaries whose values are strings)
720-
:param key: the key whose value we are processing, for error messages
721752
:param val: the value we are processing, for error messages
753+
:param must_exist: whether there must be a value
754+
:param can_have_subdict: whether the value can be a tagged string
722755
'''
723756
if isinstance(val, types.StringTypes):
724757
return cls.relative_path(val, must_exist)
@@ -793,7 +826,7 @@ def set_value(self, key, allowed_types, default_value):
793826
'''
794827
value = default_value
795828
config = self.default_config
796-
if (config != None and config.has_key(key)):
829+
if config is not None and config.has_key(key):
797830
value = config.get_value(key, allowed_types, False)
798831
self.options[key] = value
799832

@@ -809,7 +842,7 @@ def require_value(self, key, allowed_types):
809842
:type key: str
810843
'''
811844
config = self.default_config
812-
if (config == None):
845+
if config is None:
813846
raise AssertionException("No config found.")
814847
self.options[key] = value = config.get_value(key, allowed_types)
815848
return value

user_sync/connector/directory_csv.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class CSVDirectoryConnector(object):
5353
name = 'csv'
5454

5555
def __init__(self, caller_options):
56-
caller_config = user_sync.config.DictConfig('"%s options"' % CSVDirectoryConnector.name, caller_options)
56+
caller_config = user_sync.config.DictConfig('%s configuration' % self.name, caller_options)
5757
builder = user_sync.config.OptionsBuilder(caller_config)
5858
builder.set_string_value('delimiter', None)
5959
builder.set_string_value('first_name_column_name', 'firstname')
@@ -65,18 +65,16 @@ def __init__(self, caller_options):
6565
builder.set_string_value('domain_column_name', 'domain')
6666
builder.set_string_value('identity_type_column_name', 'type')
6767
builder.set_string_value('user_identity_type', None)
68-
builder.set_string_value('logger_name', CSVDirectoryConnector.name)
68+
builder.set_string_value('logger_name', self.name)
6969
builder.require_string_value('file_path')
7070
options = builder.get_options()
71-
72-
# identity type for new users if not specified in column
73-
self.user_identity_type = user_sync.identity_type.parse_identity_type(options['user_identity_type'])
74-
7571
self.options = options
76-
self.logger = logger = user_sync.connector.helper.create_logger(options)
72+
self.logger = logger = user_sync.connector.helper.create_logger(options)
73+
logger.debug('%s initialized with options: %s', self.name, options)
7774
caller_config.report_unused_values(logger)
7875

79-
logger.debug('Initialized with options: %s', options)
76+
# identity type for new users if not specified in column
77+
self.user_identity_type = user_sync.identity_type.parse_identity_type(options['user_identity_type'])
8078

8179
def load_users_and_groups(self, groups, extended_attributes):
8280
'''
@@ -129,7 +127,7 @@ def get_column_name(key):
129127
email = self.get_column_value(row, email_column_name)
130128
if email is None or email.find('@') < 0:
131129
logger.warning('Missing or invalid email at row: %d; skipping', line_read)
132-
continue;
130+
continue
133131

134132
user = users.get(email)
135133
if user is None:

0 commit comments

Comments
 (0)