Skip to content

Commit a17d4f1

Browse files
committed
finish conversion to v2 format.
All the main functionality has been converted and tested, but the Nosetests are still broken. * fix #95: overhaul config settings & arguments * fix #121: --user-filter regex matches entire user. * fix #123: recognize v1 config files & err gracefully. * change "org" to umapi * fix write-stray-list (was not using new stray functions) * fix add_umapi_user to do the right thing * make sure we flush connections after adding users * get rid of try_and_update_umapi_user - yay! * update the release notes * change the version to 2.0rc1
1 parent 1698a73 commit a17d4f1

File tree

9 files changed

+236
-176
lines changed

9 files changed

+236
-176
lines changed

RELEASE_NOTES.md

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,42 @@
11
# Release Notes for User Sync Tool Version 1.2
22

3-
These notes apply to 1.2rc1 of 2017-03-20.z
3+
These notes apply to 2.0rc1 of 2017-04-03.
4+
5+
## New Arguments & Configuration Syntax
6+
7+
There has been an extensive overhaul of both the configuration file
8+
syntax and the command-line argument syntax. See
9+
[Issue 95](https://github.com/adobe-apiplatform/user-sync.py/issues/95)
10+
and the [docs](https://adobe-apiplatform.github.io/user-sync.py/)
11+
for details.
412

513
## New Features
614

715
1. You can now exclude dashboard users from being updated or
8-
deleted by User Sync. See the
9-
[docs](https://adobe-apiplatform.github.io/user-sync.py/) for
10-
details.
16+
deleted by User Sync. See the
17+
[docs](https://adobe-apiplatform.github.io/user-sync.py/) for
18+
details.
1119
2. There is more robust reporting for errors in configuration
12-
files.
20+
files.
1321
3. The log now reports the User Sync version and gives the
14-
details of how it was invoked.
22+
details of how it was invoked.
1523
4. You can now create and manage users of all identity types,
16-
including Adobe IDs, both when operating from an LDAP
17-
directory and from CSV files.
24+
including Adobe IDs, both when operating from an LDAP
25+
directory and from CSV files.
1826
5. You can now distinguish, when a customer directory user is
19-
disabled or removed, whether to remove the matching Adobe-side
20-
from the organization or also to delete his Adobe user
21-
account.
27+
disabled or removed, whether to remove the matching Adobe-side
28+
user's product configurations and user groups, to remove the
29+
user but leave his cloud storage, or to delete his storage as well.
2230

2331
## Significant Bug Fixes
2432

2533
1. There were many bugs fixed related to managing users of
26-
identity types other than Federated ID.
34+
identity types other than Federated ID.
2735
2. There were many bugs fixes related to managing group
28-
membership of all identity types.
36+
membership of all identity types.
37+
3. There was a complete overhaul of how users who have
38+
adobe group memberships in multiple organizations are
39+
managed.
2940

3041
## Changes in Behavior
3142

@@ -34,5 +45,39 @@ some had applied only to Federated ID and some to Enterprise ID.
3445

3546
## Compatibility with Prior Versions
3647

37-
Other than as noted above, existing configuration files and
38-
should work and have the same behavior.
48+
All existing configuration files, user input files,
49+
and command-line scripts will need to be revamped
50+
to be compatible with the new formats. Here is a quick
51+
cheat sheet of what needs to be done:
52+
53+
* replace `dashboard:` with `adobe_users:`
54+
* replace `directory:` with `directory_users:`
55+
* add a `connectors:` section under `adobe_users:` similar
56+
to the one under `directory_users`
57+
* change `owning` to be `umapi` and put it under `connectors`
58+
* if you access multiple organizations, remove
59+
`secondaries`, and put
60+
all the umapi specifications under `umapi` as a list,
61+
like this:
62+
```yaml
63+
adobe_users:
64+
connectors:
65+
umapi:
66+
- primary-config.yml
67+
- org1: org1-config.yml
68+
- org2: org2-config.yml
69+
```
70+
* change `dashboard_groups` to `adobe_groups`
71+
* under `limits`, change `max_missing_users` to
72+
`max_adobe_only_users` and remove all other
73+
settings
74+
* if you have an extension, do the following:
75+
* remove the per-context: user setting
76+
* move all the settings under it to the top level in
77+
a new file, call it `extension.yaml`
78+
* change `extensions` to `extension`, move it into
79+
the `directory_users` section, and put the relative
80+
path to the new `extension.yaml` file as its value.
81+
82+
83+

tests/config_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ def test_get_dict_from_sources_str_found(self):
4646
@mock.patch('user_sync.identity_type.parse_identity_type')
4747
def test_get_rule_options(self, mock_id_type,mock_get_dict,mock_get_list,mock_get_string):
4848
mock_id_type.return_value = 'new_acc'
49-
mock_get_dict.return_value = tests.helper.MockGetString()
50-
mock_get_list.return_value = tests.helper.MockGetString()
49+
mock_get_dict.return_value = tests.helper.MockDictConfig()
50+
mock_get_list.return_value = tests.helper.MockDictConfig()
5151
options = self.conf_load.get_rule_options()
5252
expected = {
5353
'after_mapping_hook': None,

tests/helper.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ def assert_equal_users(unit_test, expected_users, actual_users):
5959
for expected_user in expected_users:
6060
actual_user = actual_users_by_email.get(expected_user['email'])
6161
unit_test.assertIsNotNone(expected_user)
62-
assert_equal_field_values(unit_test, expected_user, actual_user, ['firstname', 'lastname', 'email', 'country'])
62+
assert_equal_field_values(unit_test, expected_user, actual_user,
63+
['firstname', 'lastname', 'email', 'country'])
6364
unit_test.assertSetEqual(set(expected_user['groups']), set(actual_user['groups']))
6465

6566
def assert_equal_umapi_commands(unit_test, expected_commands, actual_commands):
@@ -75,7 +76,8 @@ def assert_equal_umapi_commands_list(unit_test, expected_commands_list, actual_c
7576
assert_equal_umapi_commands(unit_test, expected_commands, actual_commands)
7677

7778
def create_umapi_commands(user, identity_type = user_sync.identity_type.ENTERPRISE_IDENTITY_TYPE):
78-
commands = Commands(identity_type=identity_type, email=user['email'], username=user['username'], domain=user['domain'])
79+
commands = Commands(identity_type=identity_type,
80+
email=user['email'], username=user['username'], domain=user['domain'])
7981
return commands
8082

8183
def create_logger():
@@ -84,7 +86,7 @@ def create_logger():
8486
def create_action_manager():
8587
return ActionManager(None, "test org id", create_logger())
8688

87-
class MockGetString():
89+
class MockDictConfig():
8890
def get_string(self,test1,test2):
8991
return 'test'
9092

tests/rules_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def _do_country_code_test(self, mock_umapi_commands, mock_connectors, identity_t
121121
'email': '[email protected]',
122122
'uid': '001'}
123123
}
124-
mock_rules.add_umapi_user(user_key, mock_connectors)
124+
mock_rules.add_umapi_user(user_key, set(), mock_connectors)
125125

126126
if (identity_type == 'federatedID' and default_country_code == None and user_country_code == None):
127127
mock_rules.logger.error.assert_called_with('User %s cannot be added as it has a blank country code and no default has been specified.', user_key)

user_sync/app.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,11 @@ def begin_work(config_loader):
183183
directory_connector_options['user_identity_type'] = rule_config['new_account_type']
184184
directory_connector.initialize(directory_connector_options)
185185

186-
umapi_primary_connector = user_sync.connector.umapi.UmapiConnector("primary", primary_umapi_config)
186+
primary_name = '.primary' if secondary_umapi_configs else ''
187+
umapi_primary_connector = user_sync.connector.umapi.UmapiConnector(primary_name, primary_umapi_config)
187188
umapi_other_connectors = {}
188189
for secondary_umapi_name, secondary_config in secondary_umapi_configs.iteritems():
189-
umapi_secondary_conector = user_sync.connector.umapi.UmapiConnector("secondary.%s" % secondary_umapi_name,
190+
umapi_secondary_conector = user_sync.connector.umapi.UmapiConnector(".secondary.%s" % secondary_umapi_name,
190191
secondary_config)
191192
umapi_other_connectors[secondary_umapi_name] = umapi_secondary_conector
192193
umapi_connectors = user_sync.rules.UmapiConnectors(umapi_primary_connector, umapi_other_connectors)

user_sync/config.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,28 @@ def get_logging_config(self):
7777
return self.main_config.get_dict_config('logging', True)
7878

7979
def get_umapi_options(self):
80+
'''
81+
Read and return the primary and secondary umapi connector configs.
82+
The primary is a singleton, the secondaries are a map from name to config.
83+
The syntax in the config file is rather complex, which makes this code a bit complex;
84+
be sure you read the detailed docs before trying to read this function.
85+
We also check for and err out gracefully if it's a v1-style config file.
86+
:return: tuple: (primary, secondary_map)
87+
'''
88+
if self.main_config.get_dict_config('dashboard', True):
89+
raise AssertionException("Your main configuration file is still in v1 format. Please convert it to v2.")
8090
adobe_users_config = self.main_config.get_dict_config('adobe_users', True)
81-
connector_config = adobe_users_config and adobe_users_config.get_dict_config('connectors', True)
82-
if connector_config:
83-
umapi_config = connector_config.get_list('umapi', True)
91+
if not adobe_users_config:
92+
return {}, {}
93+
connector_config = adobe_users_config.get_dict_config('connectors', True)
94+
if not connector_config:
95+
return {}, {}
96+
umapi_config = connector_config.get_list('umapi', True)
97+
if not umapi_config:
98+
return {}, {}
8499
# umapi_config is a list of strings (primary umapi source files) followed by a
85-
# list of dicts (secondary umapi source specifications, which are lists of strings)
100+
# list of dicts (secondary umapi source specifications, whose keys are umapi names
101+
# and whose values are a list of config file strings)
86102
secondary_config_sources = {}
87103
primary_config_sources = []
88104
for item in umapi_config:
@@ -140,7 +156,8 @@ def get_directory_groups(self):
140156
:rtype dict(str, list(user_sync.rules.AdobeGroup))
141157
'''
142158
adobe_groups_by_directory_group = {}
143-
159+
if self.main_config.get_dict_config('directory', True):
160+
raise AssertionException("Your main configuration file is still in v1 format. Please convert it to v2.")
144161
groups_config = None
145162
directory_config = self.main_config.get_dict_config('directory_users', True)
146163
if (directory_config != None):
@@ -174,10 +191,10 @@ def get_directory_extension_options(self):
174191
if directory_config:
175192
sources = directory_config.get_list('extension', True)
176193
if sources:
177-
options = self.get_dict_from_sources(directory_config and sources)
194+
options = DictConfig('extension', self.get_dict_from_sources(sources))
178195
if options:
179-
after_mapping_hook_text = options.get('after_mapping_hook')
180-
if not after_mapping_hook_text:
196+
after_mapping_hook_text = options.get_string('after_mapping_hook', True)
197+
if after_mapping_hook_text is None:
181198
raise AssertionError("No after_mapping_hook found in extension configuration")
182199
return options
183200

@@ -293,15 +310,15 @@ def get_rule_options(self):
293310
# now get the directory extension, if any
294311
after_mapping_hook = None
295312
extended_attributes = None
296-
extension_options = self.get_directory_extension_options()
297-
if extension_options:
298-
after_mapping_hook_text = extension_options.get_string('after_mapping_hook')
313+
extension_config = self.get_directory_extension_options()
314+
if extension_config:
315+
after_mapping_hook_text = extension_config.get_string('after_mapping_hook')
299316
after_mapping_hook = compile(after_mapping_hook_text, '<per-user after-mapping-hook>', 'exec')
300-
extended_attributes = extension_options.get_list('extended_attributes')
317+
extended_attributes = extension_config.get_list('extended_attributes')
301318
# declaration of extended adobe groups: this is needed for two reasons:
302319
# 1. it allows validation of group names, and matching them to adobe groups
303320
# 2. it allows removal of adobe groups not assigned by the hook
304-
for extended_adobe_group in extension_options.get_list('extended_adobe_groups'):
321+
for extended_adobe_group in extension_config.get_list('extended_adobe_groups'):
305322
group = user_sync.rules.AdobeGroup.create(extended_adobe_group)
306323
if group is None:
307324
message = 'Extension contains illegal extended_adobe_group spec: ' + str(extended_adobe_group)

user_sync/connector/umapi.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __init__(self, name, caller_options):
4545
'''
4646
caller_config = user_sync.config.DictConfig('"%s umapi options"' % name, caller_options)
4747
builder = user_sync.config.OptionsBuilder(caller_config)
48-
builder.set_string_value('logger_name', 'umapi.' + name)
48+
builder.set_string_value('logger_name', 'umapi' + name)
4949
builder.set_bool_value('test_mode', False)
5050
options = builder.get_options()
5151

@@ -234,10 +234,16 @@ def __init__(self, connection, org_id, logger):
234234
:type org_id: str
235235
:type logger: logging.Logger
236236
'''
237+
self.action_count = 0
238+
self.error_count = 0
237239
self.items = []
238240
self.connection = connection
239241
self.org_id = org_id
240242
self.logger = logger.getChild('action')
243+
244+
def get_statistics(self):
245+
'''Return the count of actions sent so far, and how many had errors.'''
246+
return self.action_count, self.error_count
241247

242248
def get_next_request_id(self):
243249
request_id = 'action_%d' % ActionManager.next_request_id
@@ -279,6 +285,7 @@ def add_action(self, action, callback = None):
279285
'callback': callback
280286
}
281287
self.items.append(item)
288+
self.action_count += 1
282289
self.logger.log(logging.INFO, 'Added action: %s', json.dumps(action.wire_dict()))
283290
self._execute_action(action)
284291

@@ -308,6 +315,7 @@ def process_sent_items(self, total_sent):
308315
is_success = not action_errors or len(action_errors) == 0
309316

310317
if (not is_success):
318+
self.error_count += 1
311319
for error in action_errors:
312320
self.logger.error('Error in requestID: %s (User: %s, Command: %s): code: "%s" message: "%s"',
313321
action.frame.get("requestID"),

0 commit comments

Comments
 (0)