Skip to content

Commit c2758eb

Browse files
authored
Merge pull request #33 from Metasemi/morr-cxmap
Pull Mike's extensions work into v1
2 parents 74514f4 + 4142bf3 commit c2758eb

7 files changed

Lines changed: 199 additions & 36 deletions

File tree

docs/User Sync Guide.docx

-162 KB
Binary file not shown.

docs/User Sync Guide.pdf

-1.41 MB
Binary file not shown.

user_sync/config.py

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,10 @@ def get_directory_groups(self):
205205

206206
dashboard_groups_config = item.get_list_config('dashboard_groups')
207207
for dashboard_group in dashboard_groups_config.iter_values(types.StringTypes):
208-
parts = dashboard_group.split(GROUP_NAME_DELIMITER)
209-
group_name = parts.pop()
210-
organization_name = GROUP_NAME_DELIMITER.join(parts)
211-
if (len(organization_name) == 0):
212-
organization_name = user_sync.rules.OWNING_ORGANIZATION_NAME
213-
if (len(group_name) == 0):
208+
group = self.create_dashboard_group(dashboard_group)
209+
if (group is None):
214210
validation_message = 'Bad dashboard group: "%s" in directory group: "%s"' % (dashboard_group, directory_group)
215-
raise user_sync.error.AssertionException(validation_message)
216-
group = user_sync.rules.Group(group_name, organization_name)
211+
raise user_sync.error.AssertionException(validation_message)
217212
groups.append(group)
218213

219214
return adobe_groups_by_directory_group
@@ -324,6 +319,40 @@ def get_rule_options(self):
324319
max_deletions_per_run = limits_config.get_int('max_deletions_per_run')
325320
max_missing_users = limits_config.get_int('max_missing_users')
326321

322+
after_mapping_hook = None
323+
extended_attributes = None
324+
extensions_config = self.main_config.get_list_config('extensions', True)
325+
if (extensions_config != None):
326+
for extension_config in extensions_config.iter_dict_configs():
327+
context = extension_config.get_string('context')
328+
if context == 'per-user':
329+
if (after_mapping_hook == None):
330+
after_mapping_hook_text = extension_config.get_string('after_mapping_hook')
331+
if (after_mapping_hook_text is not None):
332+
after_mapping_hook = compile(after_mapping_hook_text, '<per-user after-mapping-hook>', 'exec')
333+
extended_attributes = extension_config.get_list('extended_attributes')
334+
335+
# [TODO morr 2017-02-27]: Do we really need to pre-create extended dashboard groups here? Or
336+
# could it be done on the fly, when they're encountered in values returned from hook code? If
337+
# the latter, we could do the customer a big favor by not requiring them to be declared in the
338+
# extension config.
339+
#
340+
# This should be revisited once the Complex Mapping feature as a whole is working and has been
341+
# thoroughly tested.
342+
#
343+
for extended_dashboard_group in extension_config.get_list('extended_dashboard_groups'):
344+
group = self.create_dashboard_group(extended_dashboard_group)
345+
if (group is None):
346+
validation_message = 'Bad dashboard group: "%s" in extension with context "%s"' % (extended_dashboard_group, context)
347+
raise user_sync.error.AssertionException(validation_message)
348+
349+
else:
350+
self.logger.warning("No valid hook found in extension with context '%s'; extension ignored")
351+
else:
352+
self.logger.warning("Duplicate extension context '%s' ignored", context)
353+
else:
354+
self.logger.warning("Unrecognized extension context '%s' ignored", context)
355+
327356
options = self.options
328357
result = {
329358
'directory_group_filter': options['directory_group_filter'],
@@ -336,7 +365,9 @@ def get_rule_options(self):
336365
'remove_nonexistent_users': options['remove_nonexistent_users'],
337366
'default_country_code': default_country_code,
338367
'max_deletions_per_run': max_deletions_per_run,
339-
'max_missing_users': max_missing_users
368+
'max_missing_users': max_missing_users,
369+
'after_mapping_hook': after_mapping_hook,
370+
'extended_attributes': extended_attributes,
340371
}
341372
return result
342373

@@ -351,7 +382,23 @@ def create_dashboard_options(self, connector_config_sources, owner):
351382
connector_config['enterprise'] = new_enterprise_section
352383

353384
return connector_config
354-
385+
386+
def create_dashboard_group(self, dashboard_group_qualified_name):
387+
parts = dashboard_group_qualified_name.split(GROUP_NAME_DELIMITER)
388+
group_name = parts.pop()
389+
organization_name = GROUP_NAME_DELIMITER.join(parts)
390+
if (len(organization_name) == 0):
391+
organization_name = user_sync.rules.OWNING_ORGANIZATION_NAME
392+
393+
group = None
394+
if (len(group_name) > 0):
395+
# check for existing group in case someone mistakenly declared an extended group that's already a mapping target
396+
group = user_sync.rules.Group.get_dashboard_group(group_name, organization_name)
397+
if group is None:
398+
group = user_sync.rules.Group(group_name, organization_name)
399+
400+
return group
401+
355402
def check_unused_config_keys(self):
356403
directory_connectors_config = self.get_directory_connector_configs()
357404
self.main_config.report_unused_values(self.logger, [directory_connectors_config])

user_sync/connector/directory.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ def initialize(self, options = {}):
4040
'''
4141
self.state = self.implementation.connector_initialize(options)
4242

43-
def load_users_and_groups(self, groups):
43+
def load_users_and_groups(self, groups, extended_attributes=None):
4444
'''
4545
:type groups: list(str)
46+
:type extended_attributes: list(str)
4647
:rtype (bool, iterable(dict))
4748
'''
48-
return self.implementation.connector_load_users_and_groups(self.state, groups)
49+
return self.implementation.connector_load_users_and_groups(self.state, groups, extended_attributes)
4950

user_sync/connector/directory_csv.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@ def connector_initialize(options):
3737
state = CSVDirectoryConnector(options)
3838
return state
3939

40-
def connector_load_users_and_groups(state, groups):
40+
def connector_load_users_and_groups(state, groups, extended_attributes):
4141
'''
4242
:type state: CSVDirectoryConnector
4343
:type groups: list(str)
44+
:type extended_attributes: list(str)
4445
:rtype (bool, iterable(dict))
4546
'''
47+
48+
# CSV supports arbitrary aka "extended" attrs by default, so the value of extended_attributes has no impact on this particular connector
49+
4650
return state.load_users_and_groups(groups)
4751

4852
class CSVDirectoryConnector(object):

user_sync/connector/directory_ldap.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ def connector_initialize(options):
3939
connector = LDAPDirectoryConnector(options)
4040
return connector
4141

42-
def connector_load_users_and_groups(state, groups):
42+
def connector_load_users_and_groups(state, groups, extended_attributes):
4343
'''
4444
:type state: LDAPDirectoryConnector
4545
:type groups: list(str)
46+
:type extended_attributes: list(str)
4647
:rtype (bool, iterable(dict))
4748
'''
48-
return state.load_users_and_groups(groups)
49+
return state.load_users_and_groups(groups, extended_attributes)
4950

5051
class LDAPDirectoryConnector(object):
5152
name = 'ldap'
@@ -103,9 +104,10 @@ def __init__(self, caller_options):
103104
self.connection = connection
104105
logger.info('Connected')
105106

106-
def load_users_and_groups(self, groups):
107+
def load_users_and_groups(self, groups, extended_attributes):
107108
'''
108109
:type groups: list(str)
110+
:type extended_attributes: list(str)
109111
:rtype (bool, iterable(dict))
110112
'''
111113
options = self.options
@@ -125,12 +127,12 @@ def load_users_and_groups(self, groups):
125127

126128
self.user_by_dn = user_by_dn = {}
127129
self.user_by_uid = user_by_uid = {}
128-
for user_dn, user in self.iter_users(users_filter):
130+
for user_dn, user in self.iter_users(users_filter, extended_attributes):
129131
uid = user.get('uid')
130132
if (uid != None):
131133
user_by_uid[uid] = user
132134
user_by_dn[user_dn] = user
133-
135+
134136
self.logger.info('Total users loaded: %d', len(user_by_dn))
135137

136138
for group in groups:
@@ -250,15 +252,18 @@ def iter_ldap_group_members(self, group):
250252
for attribute_value in attribute_values:
251253
yield (attribute, attribute_value)
252254

253-
def iter_users(self, users_filter):
255+
def iter_users(self, users_filter, extended_attributes):
254256
options = self.options
255257
base_dn = options['base_dn']
256-
257-
user_attribute_names = ["givenName", "sn", "c", "uid"]
258+
259+
user_attribute_names = ["givenName", "sn", "c", "uid"]
258260
user_attribute_names.extend(self.user_email_formatter.get_attribute_names())
259261
user_attribute_names.extend(self.user_username_formatter.get_attribute_names())
260262
user_attribute_names.extend(self.user_domain_formatter.get_attribute_names())
261263

264+
extended_attributes = list(set(extended_attributes) - set(user_attribute_names))
265+
user_attribute_names.extend(extended_attributes)
266+
262267
result_iter = self.iter_search_result(base_dn, ldap.SCOPE_SUBTREE, users_filter, user_attribute_names)
263268
for dn, record in result_iter:
264269
if (dn == None):
@@ -269,35 +274,53 @@ def iter_users(self, users_filter):
269274
if (last_attribute_name != None):
270275
self.logger.warn('No email attribute: %s for dn: %s', last_attribute_name, dn)
271276
continue
277+
278+
source_attributes = {}
272279

273280
user = user_sync.connector.helper.create_blank_user()
281+
source_attributes['email'] = email
274282
user['email'] = email
275-
283+
276284
username, last_attribute_name = self.user_username_formatter.generate_value(record)
285+
source_attributes['username'] = username
277286
if (username == None and last_attribute_name != None):
278287
self.logger.info('No username attribute: %s for dn: %s', last_attribute_name, dn)
279288
user['username'] = username if username != None else email
280-
289+
281290
domain, last_attribute_name = self.user_domain_formatter.generate_value(record)
291+
source_attributes['domain'] = domain
282292
if (domain != None):
283293
user['domain'] = domain
284294
elif (last_attribute_name != None):
285295
self.logger.info('No domain attribute: %s for dn: %s', last_attribute_name, dn)
286296

287297
given_name_value = LDAPValueFormatter.get_attribute_value(record, 'givenName')
288-
if (given_name_value != None):
298+
source_attributes['givenName'] = given_name_value
299+
if (given_name_value != None):
289300
user['firstname'] = given_name_value
290301
sn_value = LDAPValueFormatter.get_attribute_value(record, 'sn')
302+
source_attributes['sn'] = sn_value
291303
if sn_value != None:
292304
user['lastname'] = sn_value
293305
c_value = LDAPValueFormatter.get_attribute_value(record, 'c')
306+
source_attributes['c'] = c_value
294307
if c_value != None:
295308
user['country'] = c_value
296-
309+
297310
uid = LDAPValueFormatter.get_attribute_value(record, 'uid')
311+
source_attributes['uid'] = uid
298312
if (uid != None):
299313
user['uid'] = uid
300-
314+
315+
if extended_attributes is not None:
316+
for extended_attribute in extended_attributes:
317+
extended_attribute_value = LDAPValueFormatter.get_attribute_value(record, extended_attribute)
318+
source_attributes[extended_attribute] = extended_attribute_value
319+
320+
# [TODO morr 2017-02-26]: Could be omitted if no hook; worth considering?
321+
# [TODO morr 2017-02-28]: Is the copy necessary? Could just assign I think
322+
user['source_attributes'] = source_attributes.copy()
323+
301324
yield (dn, user)
302325

303326
def iter_search_result(self, base_dn, scope, filter_string, attributes):

0 commit comments

Comments
 (0)