Skip to content

Commit d63876f

Browse files
Merge pull request #405 from adorton-adobe/feature/group-sync-updates
Additional group sync updates
2 parents 4d006ac + 5cfaf6d commit d63876f

File tree

4 files changed

+162
-95
lines changed

4 files changed

+162
-95
lines changed

docs/en/user-manual/advanced_configuration.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -699,8 +699,6 @@ Possible use cases:
699699
* ACL groups for [Adobe Experience Manager](https://www.adobe.com/marketing/experience-manager.html)
700700
* Special-case group, role or profile assignment
701701

702-
Note: This feature only works with the LDAP connector at this time.
703-
704702
### Additional Group Rules
705703

706704
`additional_groups` is defined in `user-sync-config.yml` in the `groups`
@@ -710,6 +708,9 @@ how corresponding Adobe groups should be named. Groups that are
710708
discovered with this feature will be added to a user's list of
711709
targeted Adobe groups.
712710

711+
**Note:** Additional group mapping will fail if a multiple source groups
712+
map to the same target group.
713+
713714
### Additional Group Example
714715

715716
Suppose an Adobe Experience Manager customer would like
@@ -758,6 +759,21 @@ will apply dynamically to any LDAP group that matches the regex
758759
be included in sync as long as they follow that naming convention -
759760
no configuration change would be needed.
760761

762+
### Targeting Secondary Orgs
763+
764+
Secondary organizations can be targeted using the additional group
765+
rules. Just add the prefix `[org_name]::` to the target group
766+
pattern.
767+
768+
```yaml
769+
additional_groups:
770+
- source: "ACL-GRP-(\\d+)"
771+
target: "org2::ACL Group \\1"
772+
```
773+
774+
Refer to [Accessing Users in Other Organizations](https://adobe-apiplatform.github.io/user-sync.py/en/user-manual/advanced_configuration.html#accessing-users-in-other-organizations)
775+
for more information.
776+
761777
## Automatic Group Creation
762778

763779
The User Sync Tool can be configured to automatically create targeted
@@ -794,6 +810,14 @@ If the Sync Tool is configured to target a misspelled profile name, or
794810
a profile that doesn't exist, it will automatically create a user group
795811
with the specified name.
796812

813+
### Targeting Secondary Orgs
814+
815+
Groups targeted to secondary organizations will be automatically
816+
created on those organizations if `auto_create` is enabled.
817+
818+
Refer to [Accessing Users in Other Organizations](https://adobe-apiplatform.github.io/user-sync.py/en/user-manual/advanced_configuration.html#accessing-users-in-other-organizations)
819+
for more information.
820+
797821
---
798822

799823
[Previous Section](usage_scenarios.md) \| [Next Section](deployment_best_practices.md)

user_sync/config.py

Lines changed: 63 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -444,68 +444,73 @@ def get_rule_options(self):
444444
options.update(self.invocation_options)
445445

446446
# process directory configuration options
447-
new_account_type = None
448447
directory_config = self.main_config.get_dict_config('directory_users', True)
449-
if directory_config:
450-
# account type
451-
new_account_type = directory_config.get_string('user_identity_type', True)
452-
new_account_type = user_sync.identity_type.parse_identity_type(new_account_type)
453-
if new_account_type:
454-
options['new_account_type'] = new_account_type
455-
else:
456-
self.logger.debug("Using default for new_account_type: %s", options['new_account_type'])
457-
# country code
458-
default_country_code = directory_config.get_string('default_country_code', True)
459-
if default_country_code:
460-
options['default_country_code'] = default_country_code
461-
additional_groups = directory_config.get_list('additional_groups', True) or []
462-
additional_groups = [{'source': re.compile(r['source']), 'target': r['target']} for r in additional_groups]
463-
options['additional_groups'] = additional_groups
464-
sync_options = directory_config.get_dict_config('group_sync_options', True)
465-
if sync_options:
466-
options['auto_create'] = sync_options.get_bool('auto_create', True)
467-
if not new_account_type:
468-
new_account_type = user_sync.identity_type.ENTERPRISE_IDENTITY_TYPE
469-
self.logger.debug("Using default for new_account_type: %s", new_account_type)
448+
if not directory_config:
449+
raise AssertionException("'directory_users' must be specified")
450+
451+
# account type
452+
new_account_type = directory_config.get_string('user_identity_type', True)
453+
new_account_type = user_sync.identity_type.parse_identity_type(new_account_type)
454+
if new_account_type:
455+
options['new_account_type'] = new_account_type
456+
else:
457+
self.logger.debug("Using default for new_account_type: %s", options['new_account_type'])
458+
# country code
459+
default_country_code = directory_config.get_string('default_country_code', True)
460+
if default_country_code:
461+
options['default_country_code'] = default_country_code
462+
additional_groups = directory_config.get_list('additional_groups', True) or []
463+
try:
464+
additional_groups = [{'source': re.compile(r['source']),
465+
'target': user_sync.rules.AdobeGroup.create(r['target'], index=False)}
466+
for r in additional_groups]
467+
except Exception as e:
468+
raise AssertionException("Additional group rule error: {}".format(str(e)))
469+
options['additional_groups'] = additional_groups
470+
sync_options = directory_config.get_dict_config('group_sync_options', True)
471+
if sync_options:
472+
options['auto_create'] = sync_options.get_bool('auto_create', True)
470473

471474
# process exclusion configuration options
472475
adobe_config = self.main_config.get_dict_config('adobe_users', True)
473-
if adobe_config:
474-
exclude_identity_type_names = adobe_config.get_list('exclude_identity_types', True)
475-
if exclude_identity_type_names:
476-
exclude_identity_types = []
477-
for name in exclude_identity_type_names:
478-
message_format = 'Illegal value in exclude_identity_types: %s'
479-
identity_type = user_sync.identity_type.parse_identity_type(name, message_format)
480-
exclude_identity_types.append(identity_type)
481-
options['exclude_identity_types'] = exclude_identity_types
482-
exclude_users_regexps = adobe_config.get_list('exclude_users', True)
483-
if exclude_users_regexps:
484-
exclude_users = []
485-
for regexp in exclude_users_regexps:
486-
try:
487-
# add "match begin" and "match end" markers to ensure complete match
488-
# and compile the patterns because we will use them over and over
489-
exclude_users.append(re.compile(r'\A' + regexp + r'\Z', re.UNICODE))
490-
except re.error as e:
491-
validation_message = ('Illegal regular expression (%s) in %s: %s' %
492-
(regexp, 'exclude_identity_types', e))
493-
raise AssertionException(validation_message)
494-
options['exclude_users'] = exclude_users
495-
exclude_group_names = adobe_config.get_list('exclude_adobe_groups', True) or []
496-
if exclude_group_names:
497-
exclude_groups = []
498-
for name in exclude_group_names:
499-
group = user_sync.rules.AdobeGroup.create(name)
500-
if not group or group.get_umapi_name() != user_sync.rules.PRIMARY_UMAPI_NAME:
501-
validation_message = 'Illegal value for %s in config file: %s' % ('exclude_groups', name)
502-
if not group:
503-
validation_message += ' (Not a legal group name)'
504-
else:
505-
validation_message += ' (Can only exclude groups in primary organization)'
506-
raise AssertionException(validation_message)
507-
exclude_groups.append(group.get_group_name())
508-
options['exclude_groups'] = exclude_groups
476+
if not adobe_config:
477+
raise AssertionException("'adobe_users' must be specified")
478+
479+
exclude_identity_type_names = adobe_config.get_list('exclude_identity_types', True)
480+
if exclude_identity_type_names:
481+
exclude_identity_types = []
482+
for name in exclude_identity_type_names:
483+
message_format = 'Illegal value in exclude_identity_types: %s'
484+
identity_type = user_sync.identity_type.parse_identity_type(name, message_format)
485+
exclude_identity_types.append(identity_type)
486+
options['exclude_identity_types'] = exclude_identity_types
487+
exclude_users_regexps = adobe_config.get_list('exclude_users', True)
488+
if exclude_users_regexps:
489+
exclude_users = []
490+
for regexp in exclude_users_regexps:
491+
try:
492+
# add "match begin" and "match end" markers to ensure complete match
493+
# and compile the patterns because we will use them over and over
494+
exclude_users.append(re.compile(r'\A' + regexp + r'\Z', re.UNICODE))
495+
except re.error as e:
496+
validation_message = ('Illegal regular expression (%s) in %s: %s' %
497+
(regexp, 'exclude_identity_types', e))
498+
raise AssertionException(validation_message)
499+
options['exclude_users'] = exclude_users
500+
exclude_group_names = adobe_config.get_list('exclude_adobe_groups', True) or []
501+
if exclude_group_names:
502+
exclude_groups = []
503+
for name in exclude_group_names:
504+
group = user_sync.rules.AdobeGroup.create(name)
505+
if not group or group.get_umapi_name() != user_sync.rules.PRIMARY_UMAPI_NAME:
506+
validation_message = 'Illegal value for %s in config file: %s' % ('exclude_groups', name)
507+
if not group:
508+
validation_message += ' (Not a legal group name)'
509+
else:
510+
validation_message += ' (Can only exclude groups in primary organization)'
511+
raise AssertionException(validation_message)
512+
exclude_groups.append(group.get_group_name())
513+
options['exclude_groups'] = exclude_groups
509514

510515
# get the limits
511516
limits_config = self.main_config.get_dict_config('limits')

user_sync/connector/directory_ldap.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -292,17 +292,7 @@ def iter_users(self, users_filter, extended_attributes):
292292
elif last_attribute_name:
293293
self.logger.warning('No country code attribute (%s) for user with dn: %s', last_attribute_name, dn)
294294

295-
uid_value = LDAPValueFormatter.get_attribute_value(record, six.text_type('uid'))
296-
source_attributes['uid'] = uid_value
297-
298-
user['member_groups'] = []
299-
if self.additional_group_filters:
300-
member_groups = []
301-
for f in self.additional_group_filters:
302-
for g in self.get_member_groups(record):
303-
if f.match(g) and g not in member_groups:
304-
member_groups.append(g)
305-
user['member_groups'] = member_groups
295+
user['member_groups'] = self.get_member_groups(record) if self.additional_group_filters else []
306296

307297
if extended_attributes is not None:
308298
for extended_attribute in extended_attributes:

user_sync/rules.py

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import user_sync.connector.umapi
2727
import user_sync.error
2828
import user_sync.identity_type
29+
from collections import defaultdict
2930
from user_sync.helper import normalize_string, CSVAdapter, JobStats
3031

3132
GROUP_NAME_DELIMITER = '::'
@@ -173,10 +174,15 @@ def run(self, directory_groups, directory_connector, umapi_connectors):
173174
self.read_desired_user_groups(directory_groups, directory_connector)
174175
load_directory_stats.log_end(logger)
175176

177+
for umapi_info in self.umapi_info_by_name.values():
178+
self.validate_and_log_additional_groups(umapi_info)
179+
176180
umapi_stats = JobStats('Push to UMAPI' if self.push_umapi else 'Sync with UMAPI', divider="-")
177181
umapi_stats.log_start(logger)
178182
if directory_connector is not None:
179-
if self.options.get('process_groups'):
183+
# note: push mode is not supported because if it is, we won't have a list of groups
184+
# that exist in the console. we don't want to attempt to create groups that already exist
185+
if self.options.get('process_groups') and not self.push_umapi and self.options.get('auto_create'):
180186
self.create_umapi_groups(umapi_connectors)
181187
self.sync_umapi_users(umapi_connectors)
182188
if self.will_process_strays:
@@ -185,6 +191,20 @@ def run(self, directory_groups, directory_connector, umapi_connectors):
185191
umapi_stats.log_end(logger)
186192
self.log_action_summary(umapi_connectors)
187193

194+
def validate_and_log_additional_groups(self, umapi_info):
195+
"""
196+
:param umapi_info: UmapiTargetInfo
197+
:return:
198+
"""
199+
umapi_name = umapi_info.get_name()
200+
for mapped, src_groups in umapi_info.get_additional_group_map().items():
201+
if len(src_groups) > 1:
202+
raise user_sync.error.AssertionException(
203+
"Additional group resolution conflict: {} map to '{}' on '{}'".format(
204+
src_groups, mapped, umapi_name if umapi_name else 'primary org'))
205+
self.logger.info("Mapped additional group '{}' to '{}' on '{}'".format(
206+
src_groups[0], mapped, umapi_name if umapi_name else 'primary org'))
207+
188208
def log_action_summary(self, umapi_connectors):
189209
"""
190210
log number of affected directory and Adobe users,
@@ -394,11 +414,19 @@ def read_desired_user_groups(self, mappings, directory_connector):
394414
member_groups = directory_user.get('member_groups', [])
395415
for member_group in member_groups:
396416
for group_rule in additional_groups:
397-
if group_rule['source'].match(member_group):
398-
rename_group = group_rule['source'].sub(group_rule['target'], member_group)
399-
umapi_info.add_mapped_group(rename_group)
400-
for umapi_name, umapi_info in six.iteritems(self.umapi_info_by_name):
401-
umapi_info.add_desired_group_for(user_key, rename_group)
417+
source = group_rule['source']
418+
target = group_rule['target']
419+
target_name = target.get_group_name()
420+
umapi_info = self.get_umapi_info(target.get_umapi_name())
421+
if not group_rule['source'].match(member_group):
422+
continue
423+
try:
424+
rename_group = source.sub(target_name, member_group)
425+
except Exception as e:
426+
raise user_sync.error.AssertionException("Additional group resolution error: {}".format(str(e)))
427+
umapi_info.add_mapped_group(rename_group)
428+
umapi_info.add_additional_group(rename_group, member_group)
429+
umapi_info.add_desired_group_for(user_key, rename_group)
402430

403431
self.logger.debug('Total directory users after filtering: %d', len(filtered_directory_user_by_user_key))
404432
if self.logger.isEnabledFor(logging.DEBUG):
@@ -474,24 +502,32 @@ def create_umapi_groups(self, umapi_connectors):
474502
in the console, then it will create. Note: Push Mode is not supported
475503
:type umapi_connectors: UmapiConnectors
476504
"""
477-
if not self.push_umapi:
478-
umapi_info, umapi_connector = self.get_umapi_info(
479-
PRIMARY_UMAPI_NAME), umapi_connectors.get_primary_connector()
505+
for umapi_connector in umapi_connectors.connectors:
506+
umapi_name = None if umapi_connector.name.split('.')[-1] == 'primary'\
507+
else umapi_connector.name.split('.')[-1]
508+
if umapi_name == 'umapi':
509+
umapi_name = None
510+
if umapi_name not in self.umapi_info_by_name:
511+
continue
512+
umapi_info = self.umapi_info_by_name[umapi_name]
480513
mapped_groups = umapi_info.get_non_normalize_mapped_groups()
514+
481515
# pull all user groups from console
482516
on_adobe_groups = [normalize_string(g['groupName']) for g in umapi_connector.get_groups()]
517+
483518
# verify if group exist and create
484-
auto_create = self.options.get('auto_create', None)
485-
if auto_create:
486-
for mapped_group in mapped_groups:
487-
if normalize_string(mapped_group) not in on_adobe_groups:
488-
self.logger.info("Auto create user-group enabled: Creating %s" % mapped_group)
489-
try:
490-
# create group
491-
res = umapi_connector.create_group(mapped_group)
492-
self.action_summary['adobe_user_groups_created'] += 1
493-
except Exception as e:
494-
self.logger.critical("Unable to create %s user group: %s" % (mapped_group, e))
519+
for mapped_group in mapped_groups:
520+
if normalize_string(mapped_group) in on_adobe_groups:
521+
continue
522+
self.logger.info("Auto create user-group enabled: Creating '{}' on '{}'".format(
523+
mapped_group, umapi_name if umapi_name else 'primary org'))
524+
try:
525+
# create group
526+
res = umapi_connector.create_group(mapped_group)
527+
self.action_summary['adobe_user_groups_created'] += 1
528+
except Exception as e:
529+
self.logger.critical("Unable to create %s user group: '{}' on '{}' (error: {})".format(
530+
mapped_group, umapi_name if umapi_name else 'primary org', e))
495531

496532
def is_selected_user_key(self, user_key):
497533
"""
@@ -1115,14 +1151,15 @@ def execute_actions(self):
11151151
class AdobeGroup(object):
11161152
index_map = {}
11171153

1118-
def __init__(self, group_name, umapi_name):
1154+
def __init__(self, group_name, umapi_name, index=True):
11191155
"""
11201156
:type group_name: str
11211157
:type umapi_name: str
11221158
"""
11231159
self.group_name = group_name
11241160
self.umapi_name = umapi_name
1125-
AdobeGroup.index_map[(group_name, umapi_name)] = self
1161+
if index:
1162+
AdobeGroup.index_map[(group_name, umapi_name)] = self
11261163

11271164
def __eq__(self, other):
11281165
return self.__dict__ == other.__dict__
@@ -1166,13 +1203,13 @@ def lookup(cls, qualified_name):
11661203
return cls.index_map.get(cls._parse(qualified_name))
11671204

11681205
@classmethod
1169-
def create(cls, qualified_name):
1206+
def create(cls, qualified_name, index=True):
11701207
group_name, umapi_name = cls._parse(qualified_name)
11711208
existing = cls.index_map.get((group_name, umapi_name))
11721209
if existing:
11731210
return existing
11741211
elif len(group_name) > 0:
1175-
return cls(group_name, umapi_name)
1212+
return cls(group_name, umapi_name, index)
11761213
else:
11771214
return None
11781215

@@ -1196,6 +1233,10 @@ def __init__(self, name):
11961233
self.groups_added_by_user_key = {}
11971234
self.groups_removed_by_user_key = {}
11981235

1236+
# keep track of auto-mapped additional groups for conflict tracking.
1237+
# if feature is disabled, this dict will be empty
1238+
self.additional_group_map = defaultdict(list) # type: dict[str, list[str]]
1239+
11991240
def get_name(self):
12001241
return self.name
12011242

@@ -1207,6 +1248,13 @@ def add_mapped_group(self, group):
12071248
self.mapped_groups.add(normalized_group_name)
12081249
self.non_normalize_mapped_groups.add(group)
12091250

1251+
def add_additional_group(self, rename_group, member_group):
1252+
if member_group not in self.additional_group_map[rename_group]:
1253+
self.additional_group_map[normalize_string(rename_group)].append(member_group)
1254+
1255+
def get_additional_group_map(self):
1256+
return self.additional_group_map
1257+
12101258
def get_mapped_groups(self):
12111259
return self.mapped_groups
12121260

0 commit comments

Comments
 (0)