Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
# user-sync.py
# user-sync.py: User Sync Tool from Adobe

Application for synchronizing customer directories with the
Adobe Enterprise Admin Console via the
[User Management API](https://www.adobe.io/products/usermanagement/docs/gettingstarted.html)
(aka UMAPI).
The User Sync Tool is a command-line tool that automates the creation and management of Adobe user accounts. It
does this by reading user and group information from an organization's enterprise directory system or a file and
then creating, updating, or removing user accounts in the Adobe Admin Console. The key goals of the User Sync
Tool are to streamline the process of named user deployment and automate user management for all Adobe users and products.

This application is open source, maintained by Adobe, and distributed under the terms
of the OSI-approved MIT license. See the LICENSE file for details.

Copyright (c) 2016-2017 Adobe Systems Incorporated.

# Overview
# Quick Links

- [User Sync Overview](https://www.adobe.io/apis/cloudplatform/usermanagement/docs/UserSyncTool.html)
- [User Manual](https://adobe-apiplatform.github.io/user-sync.py/user-manual/)
- [Step-by-Step Setup](https://adobe-apiplatform.github.io/user-sync.py/success-guide/)
- [Non-Technical Overview](https://spark.adobe.com/page/E3hSsLq3G1iVz/)

`user-sync` automates user creation and product entitlement
assignment in the Adobe Enterprise Admin Console.
It takes a list of enterprise directory users,
either from an LDAP connection or from a tab-separated file,
and creates, updates, or removes user accounts in the
Admin Console.

# Requirements

Expand Down
6 changes: 4 additions & 2 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Release Notes for User Sync Tool Version 1.2
# Release Notes for User Sync Tool Version 2.0

These notes apply to 2.0rc1 of 2017-04-03.
These notes apply to 2.0rc2 of 2017-04-07.

## New Arguments & Configuration Syntax

Expand Down Expand Up @@ -79,5 +79,7 @@ a new file, call it `extension.yaml`
the `directory_users` section, and put the relative
path to the new `extension.yaml` file as its value.

If you have a file that lists users for input (--users file f) or removal, the column named `user` should be renamed to `username`.



1 change: 1 addition & 0 deletions user_sync/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ def get_rule_options(self):
'remove_strays': options['remove_strays'],
'stray_list_input_path': options['stray_list_input_path'],
'stray_list_output_path': options['stray_list_output_path'],
'test_mode': options['test_mode'],
'update_user_info': options['update_user_info'],
'username_filter_regex': options['username_filter_regex'],
}
Expand Down
98 changes: 58 additions & 40 deletions user_sync/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def __init__(self, caller_options):
'remove_strays': False,
'stray_list_input_path': None,
'stray_list_output_path': None,
'test_mode': False,
'update_user_info': True,
'username_filter_regex': None,
}
Expand Down Expand Up @@ -89,22 +90,24 @@ def __init__(self, caller_options):
# of primary-umapi users, who are presumed to be in primary-umapi domains.
# So instead of keeping track of excluded users in the primary umapi,
# we keep track of included users, so we can match them against users
# in the secondary umapis (and exclude all that don't match).
# in the secondary umapis (and exclude all that don't match). Finally,
# we keep track of user keys (in any umapi) that we have updated, so
# we can correctly report their count.
self.included_user_keys = set()
self.excluded_user_count = 0
self.updated_user_keys = set()

# stray key input path comes in, stray_list_output_path goes out
self.stray_key_map = self.make_stray_key_map()
if options['stray_list_input_path']:
self.read_stray_key_map(options['stray_list_input_path'])
self.stray_list_output_path = options['stray_list_output_path']

# determine whether we need to process strays at all
self.will_process_strays = (not options['exclude_strays']) and (options['manage_groups'] or
options['stray_list_output_path'] or
options['disentitle_strays'] or
options['remove_strays'] or
options['delete_strays'])
# determine what processing is needed on strays
self.will_manage_strays = (options['manage_groups'] or options['disentitle_strays'] or
options['remove_strays'] or options['delete_strays'])
self.will_process_strays = (not options['exclude_strays']) and (options['stray_list_output_path'] or
self.will_manage_strays)

# in/out variables for per-user after-mapping-hook code
self.after_mapping_hook_scope = {
Expand Down Expand Up @@ -171,6 +174,7 @@ def log_action_summary(self, umapi_connectors):
# find the total number of adobe users and excluded users
self.action_summary['adobe_users_read'] = len(self.included_user_keys) + self.excluded_user_count
self.action_summary['adobe_users_excluded'] = self.excluded_user_count
self.action_summary['adobe_users_updated'] = len(self.updated_user_keys)
# find out the number of users that have no changes; this depends on whether
# we actually read the directory or read an input file. So there are two cases:
if self.action_summary['adobe_users_read'] == 0:
Expand All @@ -182,7 +186,11 @@ def log_action_summary(self, umapi_connectors):
self.action_summary['adobe_users_updated'] -
self.action_summary['adobe_strays_processed']
)
logger.info('---------------------------------- Action Summary ----------------------------------')
if self.options['test_mode']:
header = '- Action Summary (TEST MODE) -'
else:
header = '------- Action Summary -------'
logger.info('---------------------------' + header + '---------------------------')

# English text description for action summary log.
# The action summary will be shown the same order as they are defined in this list
Expand All @@ -193,7 +201,7 @@ def log_action_summary(self, umapi_connectors):
['adobe_users_excluded', 'Number of Adobe users excluded from updates'],
['adobe_users_unchanged', 'Number of non-excluded Adobe users with no changes'],
['adobe_users_created', 'Number of new Adobe users added'],
['adobe_users_updated', 'Number of existing Adobe users updated'],
['adobe_users_updated', 'Number of matching Adobe users updated'],
]
if self.will_process_strays:
if self.options['delete_strays']:
Expand All @@ -204,7 +212,7 @@ def log_action_summary(self, umapi_connectors):
action = 'removed from all groups'
else:
action = 'with groups processed'
action_summary_description.append(['adobe_strays_processed', 'Number of Adobe-only users ' + action + ':'])
action_summary_description.append(['adobe_strays_processed', 'Number of Adobe-only users ' + action])

# prepare the network summary
umapi_summary_format = 'Number of%s%s UMAPI actions sent (total, success, error)'
Expand Down Expand Up @@ -436,15 +444,16 @@ def process_strays(self, umapi_connectors):
stray_count = len(self.get_stray_keys())
if self.stray_list_output_path:
self.write_stray_key_map()
max_missing = self.options['max_adobe_only_users']
if stray_count > max_missing:
self.logger.critical('Unable to process Adobe-only users, as their count (%s) is larger '
'than the max_adobe_only_users setting (%d)', stray_count, max_missing)
self.action_summary['adobe_strays_processed'] = 0
return
self.action_summary['adobe_strays_processed'] = stray_count
self.logger.debug("Processing Adobe-only users...")
self.manage_strays(umapi_connectors)
if self.will_manage_strays:
max_missing = self.options['max_adobe_only_users']
if stray_count > max_missing:
self.logger.critical('Unable to process Adobe-only users, as their count (%s) is larger '
'than the max_adobe_only_users setting (%d)', stray_count, max_missing)
self.action_summary['adobe_strays_processed'] = 0
return
self.action_summary['adobe_strays_processed'] = stray_count
self.logger.debug("Processing Adobe-only users...")
self.manage_strays(umapi_connectors)

def manage_strays(self, umapi_connectors):
'''
Expand Down Expand Up @@ -627,7 +636,7 @@ def update_umapi_user(self, umapi_info, user_key, umapi_connector,
'''
is_primary_org = umapi_info.get_name() == PRIMARY_UMAPI_NAME
if attributes_to_update or groups_to_add or groups_to_remove:
self.action_summary['adobe_users_updated'] += 1 if is_primary_org else 0
self.updated_user_keys.add(user_key)
if attributes_to_update:
self.logger.info('Updating info for user key: %s changes: %s', user_key, attributes_to_update)
if groups_to_add or groups_to_remove:
Expand Down Expand Up @@ -900,59 +909,68 @@ def read_stray_key_map(self, file_path, delimiter = None):
id_type_column_name = 'type'
user_column_name = 'username'
domain_column_name = 'domain'
org_name_column_name = 'umapi'
ummapi_name_column_name = 'umapi'
rows = user_sync.helper.iter_csv_rows(file_path,
delimiter = delimiter,
recognized_column_names = [
id_type_column_name, user_column_name, domain_column_name,
org_name_column_name,
ummapi_name_column_name,
],
logger = self.logger)
for row in rows:
org_name = row.get(org_name_column_name) or PRIMARY_UMAPI_NAME
umapi_name = row.get(ummapi_name_column_name) or PRIMARY_UMAPI_NAME
id_type = row.get(id_type_column_name)
user = row.get(user_column_name)
domain = row.get(domain_column_name)
user_key = self.get_user_key(id_type, user, domain)
if user_key:
self.add_stray(org_name, None)
self.add_stray(org_name, user_key)
self.add_stray(umapi_name, None)
self.add_stray(umapi_name, user_key)
else:
self.logger.error("Invalid input line, ignored: %s", row)
user_count = len(self.get_stray_keys())
user_plural = "" if user_count == 1 else "s"
org_count = len(self.stray_key_map) - 1
org_plural = "" if org_count == 1 else "s"
if org_count > 0:
secondary_count = len(self.stray_key_map) - 1
if secondary_count > 0:
umapi_plural = "" if secondary_count == 1 else "s"
self.logger.info('Read %d Adobe-only user%s for primary umapi, with %d secondary umapi%s',
user_count, user_plural, org_count, org_plural)
user_count, user_plural, secondary_count, umapi_plural)
else:
self.logger.info('Read %d Adobe-only user%s.', user_count, user_plural)

def write_stray_key_map(self):
file_path = self.stray_list_output_path
logger = self.logger
logger.info('Writing Adobe-only users to: %s', file_path)
# figure out if we should include a umapi column
secondary_count = 0
fieldnames = ['type', 'username', 'domain']
for umapi_name in self.stray_key_map:
if umapi_name != PRIMARY_UMAPI_NAME and self.get_stray_keys(umapi_name):
if not secondary_count:
fieldnames.append('umapi')
secondary_count += 1
with open(file_path, 'wb') as output_file:
delimiter = user_sync.helper.guess_delimiter_from_filename(file_path)
writer = csv.DictWriter(output_file,
fieldnames = ['type', 'username', 'domain', 'umapi'],
delimiter = delimiter)
writer = csv.DictWriter(output_file, fieldnames=fieldnames, delimiter=delimiter)
writer.writeheader()
# None sorts before strings, so sorting the keys in the map
# puts the primary umapi first in the output, which is handy
for org_name in sorted(self.stray_key_map.keys()):
for user_key in self.get_stray_keys(org_name):
for umapi_name in sorted(self.stray_key_map.keys()):
for user_key in self.get_stray_keys(umapi_name):
id_type, username, domain = self.parse_user_key(user_key)
umapi = org_name if org_name else ""
writer.writerow({'type': id_type, 'username': username, 'domain': domain, 'umapi': umapi})
umapi = umapi_name if umapi_name else ""
if secondary_count:
row_dict = {'type': id_type, 'username': username, 'domain': domain, 'umapi': umapi}
else:
row_dict = {'type': id_type, 'username': username, 'domain': domain}
writer.writerow(row_dict)
user_count = len(self.stray_key_map.get(PRIMARY_UMAPI_NAME, []))
user_plural = "" if user_count == 1 else "s"
org_count = len(self.stray_key_map) - 1
org_plural = "" if org_count == 1 else "s"
if org_count > 0:
if secondary_count > 0:
umapi_plural = "" if secondary_count == 1 else "s"
logger.info('Wrote %d Adobe-only user%s for primary umapi, with %d secondary umapi%s',
user_count, user_plural, org_count, org_plural)
user_count, user_plural, secondary_count, umapi_plural)
else:
logger.info('Wrote %d Adobe-only user%s.', user_count, user_plural)

Expand Down
2 changes: 1 addition & 1 deletion user_sync/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

__version__ = '2.0rc1'
__version__ = '2.0rc2'