Skip to content

Commit b06f2da

Browse files
authored
Merge pull request #161 from adobe-apiplatform/v2
Bring branch up to date
2 parents c624c0f + 2d89535 commit b06f2da

File tree

5 files changed

+75
-55
lines changed

5 files changed

+75
-55
lines changed

README.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1-
# user-sync.py
1+
# user-sync.py: User Sync Tool from Adobe
22

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

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

1111
Copyright (c) 2016-2017 Adobe Systems Incorporated.
1212

13-
# Overview
13+
# Quick Links
14+
15+
- [User Sync Overview](https://www.adobe.io/apis/cloudplatform/usermanagement/docs/UserSyncTool.html)
16+
- [User Manual](https://adobe-apiplatform.github.io/user-sync.py/user-manual/)
17+
- [Step-by-Step Setup](https://adobe-apiplatform.github.io/user-sync.py/success-guide/)
18+
- [Non-Technical Overview](https://spark.adobe.com/page/E3hSsLq3G1iVz/)
1419

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

2221
# Requirements
2322

RELEASE_NOTES.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Release Notes for User Sync Tool Version 1.2
1+
# Release Notes for User Sync Tool Version 2.0
22

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

55
## New Arguments & Configuration Syntax
66

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

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

8385

user_sync/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ def get_rule_options(self):
348348
'remove_strays': options['remove_strays'],
349349
'stray_list_input_path': options['stray_list_input_path'],
350350
'stray_list_output_path': options['stray_list_output_path'],
351+
'test_mode': options['test_mode'],
351352
'update_user_info': options['update_user_info'],
352353
'username_filter_regex': options['username_filter_regex'],
353354
}

user_sync/rules.py

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def __init__(self, caller_options):
5454
'remove_strays': False,
5555
'stray_list_input_path': None,
5656
'stray_list_output_path': None,
57+
'test_mode': False,
5758
'update_user_info': True,
5859
'username_filter_regex': None,
5960
}
@@ -89,22 +90,24 @@ def __init__(self, caller_options):
8990
# of primary-umapi users, who are presumed to be in primary-umapi domains.
9091
# So instead of keeping track of excluded users in the primary umapi,
9192
# we keep track of included users, so we can match them against users
92-
# in the secondary umapis (and exclude all that don't match).
93+
# in the secondary umapis (and exclude all that don't match). Finally,
94+
# we keep track of user keys (in any umapi) that we have updated, so
95+
# we can correctly report their count.
9396
self.included_user_keys = set()
9497
self.excluded_user_count = 0
98+
self.updated_user_keys = set()
9599

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

102-
# determine whether we need to process strays at all
103-
self.will_process_strays = (not options['exclude_strays']) and (options['manage_groups'] or
104-
options['stray_list_output_path'] or
105-
options['disentitle_strays'] or
106-
options['remove_strays'] or
107-
options['delete_strays'])
106+
# determine what processing is needed on strays
107+
self.will_manage_strays = (options['manage_groups'] or options['disentitle_strays'] or
108+
options['remove_strays'] or options['delete_strays'])
109+
self.will_process_strays = (not options['exclude_strays']) and (options['stray_list_output_path'] or
110+
self.will_manage_strays)
108111

109112
# in/out variables for per-user after-mapping-hook code
110113
self.after_mapping_hook_scope = {
@@ -171,6 +174,7 @@ def log_action_summary(self, umapi_connectors):
171174
# find the total number of adobe users and excluded users
172175
self.action_summary['adobe_users_read'] = len(self.included_user_keys) + self.excluded_user_count
173176
self.action_summary['adobe_users_excluded'] = self.excluded_user_count
177+
self.action_summary['adobe_users_updated'] = len(self.updated_user_keys)
174178
# find out the number of users that have no changes; this depends on whether
175179
# we actually read the directory or read an input file. So there are two cases:
176180
if self.action_summary['adobe_users_read'] == 0:
@@ -182,7 +186,11 @@ def log_action_summary(self, umapi_connectors):
182186
self.action_summary['adobe_users_updated'] -
183187
self.action_summary['adobe_strays_processed']
184188
)
185-
logger.info('---------------------------------- Action Summary ----------------------------------')
189+
if self.options['test_mode']:
190+
header = '- Action Summary (TEST MODE) -'
191+
else:
192+
header = '------- Action Summary -------'
193+
logger.info('---------------------------' + header + '---------------------------')
186194

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

209217
# prepare the network summary
210218
umapi_summary_format = 'Number of%s%s UMAPI actions sent (total, success, error)'
@@ -436,15 +444,16 @@ def process_strays(self, umapi_connectors):
436444
stray_count = len(self.get_stray_keys())
437445
if self.stray_list_output_path:
438446
self.write_stray_key_map()
439-
max_missing = self.options['max_adobe_only_users']
440-
if stray_count > max_missing:
441-
self.logger.critical('Unable to process Adobe-only users, as their count (%s) is larger '
442-
'than the max_adobe_only_users setting (%d)', stray_count, max_missing)
443-
self.action_summary['adobe_strays_processed'] = 0
444-
return
445-
self.action_summary['adobe_strays_processed'] = stray_count
446-
self.logger.debug("Processing Adobe-only users...")
447-
self.manage_strays(umapi_connectors)
447+
if self.will_manage_strays:
448+
max_missing = self.options['max_adobe_only_users']
449+
if stray_count > max_missing:
450+
self.logger.critical('Unable to process Adobe-only users, as their count (%s) is larger '
451+
'than the max_adobe_only_users setting (%d)', stray_count, max_missing)
452+
self.action_summary['adobe_strays_processed'] = 0
453+
return
454+
self.action_summary['adobe_strays_processed'] = stray_count
455+
self.logger.debug("Processing Adobe-only users...")
456+
self.manage_strays(umapi_connectors)
448457

449458
def manage_strays(self, umapi_connectors):
450459
'''
@@ -627,7 +636,7 @@ def update_umapi_user(self, umapi_info, user_key, umapi_connector,
627636
'''
628637
is_primary_org = umapi_info.get_name() == PRIMARY_UMAPI_NAME
629638
if attributes_to_update or groups_to_add or groups_to_remove:
630-
self.action_summary['adobe_users_updated'] += 1 if is_primary_org else 0
639+
self.updated_user_keys.add(user_key)
631640
if attributes_to_update:
632641
self.logger.info('Updating info for user key: %s changes: %s', user_key, attributes_to_update)
633642
if groups_to_add or groups_to_remove:
@@ -900,59 +909,68 @@ def read_stray_key_map(self, file_path, delimiter = None):
900909
id_type_column_name = 'type'
901910
user_column_name = 'username'
902911
domain_column_name = 'domain'
903-
org_name_column_name = 'umapi'
912+
ummapi_name_column_name = 'umapi'
904913
rows = user_sync.helper.iter_csv_rows(file_path,
905914
delimiter = delimiter,
906915
recognized_column_names = [
907916
id_type_column_name, user_column_name, domain_column_name,
908-
org_name_column_name,
917+
ummapi_name_column_name,
909918
],
910919
logger = self.logger)
911920
for row in rows:
912-
org_name = row.get(org_name_column_name) or PRIMARY_UMAPI_NAME
921+
umapi_name = row.get(ummapi_name_column_name) or PRIMARY_UMAPI_NAME
913922
id_type = row.get(id_type_column_name)
914923
user = row.get(user_column_name)
915924
domain = row.get(domain_column_name)
916925
user_key = self.get_user_key(id_type, user, domain)
917926
if user_key:
918-
self.add_stray(org_name, None)
919-
self.add_stray(org_name, user_key)
927+
self.add_stray(umapi_name, None)
928+
self.add_stray(umapi_name, user_key)
920929
else:
921930
self.logger.error("Invalid input line, ignored: %s", row)
922931
user_count = len(self.get_stray_keys())
923932
user_plural = "" if user_count == 1 else "s"
924-
org_count = len(self.stray_key_map) - 1
925-
org_plural = "" if org_count == 1 else "s"
926-
if org_count > 0:
933+
secondary_count = len(self.stray_key_map) - 1
934+
if secondary_count > 0:
935+
umapi_plural = "" if secondary_count == 1 else "s"
927936
self.logger.info('Read %d Adobe-only user%s for primary umapi, with %d secondary umapi%s',
928-
user_count, user_plural, org_count, org_plural)
937+
user_count, user_plural, secondary_count, umapi_plural)
929938
else:
930939
self.logger.info('Read %d Adobe-only user%s.', user_count, user_plural)
931940

932941
def write_stray_key_map(self):
933942
file_path = self.stray_list_output_path
934943
logger = self.logger
935944
logger.info('Writing Adobe-only users to: %s', file_path)
945+
# figure out if we should include a umapi column
946+
secondary_count = 0
947+
fieldnames = ['type', 'username', 'domain']
948+
for umapi_name in self.stray_key_map:
949+
if umapi_name != PRIMARY_UMAPI_NAME and self.get_stray_keys(umapi_name):
950+
if not secondary_count:
951+
fieldnames.append('umapi')
952+
secondary_count += 1
936953
with open(file_path, 'wb') as output_file:
937954
delimiter = user_sync.helper.guess_delimiter_from_filename(file_path)
938-
writer = csv.DictWriter(output_file,
939-
fieldnames = ['type', 'username', 'domain', 'umapi'],
940-
delimiter = delimiter)
955+
writer = csv.DictWriter(output_file, fieldnames=fieldnames, delimiter=delimiter)
941956
writer.writeheader()
942957
# None sorts before strings, so sorting the keys in the map
943958
# puts the primary umapi first in the output, which is handy
944-
for org_name in sorted(self.stray_key_map.keys()):
945-
for user_key in self.get_stray_keys(org_name):
959+
for umapi_name in sorted(self.stray_key_map.keys()):
960+
for user_key in self.get_stray_keys(umapi_name):
946961
id_type, username, domain = self.parse_user_key(user_key)
947-
umapi = org_name if org_name else ""
948-
writer.writerow({'type': id_type, 'username': username, 'domain': domain, 'umapi': umapi})
962+
umapi = umapi_name if umapi_name else ""
963+
if secondary_count:
964+
row_dict = {'type': id_type, 'username': username, 'domain': domain, 'umapi': umapi}
965+
else:
966+
row_dict = {'type': id_type, 'username': username, 'domain': domain}
967+
writer.writerow(row_dict)
949968
user_count = len(self.stray_key_map.get(PRIMARY_UMAPI_NAME, []))
950969
user_plural = "" if user_count == 1 else "s"
951-
org_count = len(self.stray_key_map) - 1
952-
org_plural = "" if org_count == 1 else "s"
953-
if org_count > 0:
970+
if secondary_count > 0:
971+
umapi_plural = "" if secondary_count == 1 else "s"
954972
logger.info('Wrote %d Adobe-only user%s for primary umapi, with %d secondary umapi%s',
955-
user_count, user_plural, org_count, org_plural)
973+
user_count, user_plural, secondary_count, umapi_plural)
956974
else:
957975
logger.info('Wrote %d Adobe-only user%s.', user_count, user_plural)
958976

user_sync/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
# SOFTWARE.
2020

21-
__version__ = '2.0rc1'
21+
__version__ = '2.0rc2'

0 commit comments

Comments
 (0)