Skip to content

Commit 273e16b

Browse files
authored
Merge pull request #37 from adobe-apiplatform/v1
v1.1 - third try
2 parents 0a5207d + aa4b27f commit 273e16b

15 files changed

+439
-142
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ __pycache__/
99
# Distribution / packaging
1010
.Python
1111
env/
12-
build/
12+
/build/
1313
develop-eggs/
1414
dist/
1515
downloads/

RELEASE_NOTES.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Release Notes for User Sync Tool Version 1.1
2+
2017-03-03.
3+
4+
## Updating from prior versions
5+
6+
This version has 2 new required configuration items. If you are using an earlier version, you will need to add the following entires to your main user-sync-config.yaml file:
7+
8+
limits:
9+
max_deletions_per_run: 10 # if --remove-nonexistent-users is specified, this is the most users that will be removed. Others will be left for a later run. A critical message will be logged.
10+
max_missing_users: 200 # if more than this number of user accounts are not found in the directory, user sync will abort with an error and a critical message will be logged.
11+
12+
To update your installation, download the release for your platform and replace the user-sync or user-sync.pex file with the new one. Same the old one in case of problems
13+
14+
Because this version contains a more aggressive --process-groups function (a bug fix) you may want to run with --test-only first and make sure nothing unexpected is happening.
15+
16+
## New Features
17+
18+
1. Ability to specify additional directory attributes to load and specify a code snippet to implement complex mappings from directory information to Adobe user information and group membership. This is covered in more detail in the updated documentastion.
19+
20+
2. Releases for different platforms are packaged separately so you only have to download the platform(s) you want. You do have to download the example configuration files and documentation separately.
21+
22+
3. User removal limits and guards. There are some new features to prevent user sync from accidentally removing large numbers of users in the event of misconfiguration or changes in the directory which result in users not being returned from queries.
23+
24+
25+
26+
## Changes in Behavior
27+
28+
A bug in --process-groups was fixed. Previously, users present in the Adobe admin console but not in the directory were not removed from groups that were mapped in the user sync configuration file. This is now fixed. A group (user group or product configuration) that is mapped from a directory group is assumed to be under user sync control and any users in such groups that are not in the directory and in groups in the directory mapped to those Adobe groups are removed.
29+
30+
This release of user sync should be compatible and have no other behavior changes.
31+
32+
## Compatibility with Prior Versions
33+
34+
Other than as noted above, existing configuration files and should work and have the same behavior.

examples/example.user-sync-config.yml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
dashboard:
22
# specifies the configurations for the Adobe Enterprise Dashboards.
33
# By default, it would look for dashboard-owning-config.yml and
4-
# dashboard-trustee-*-config.yml in the configuration path,
5-
# with the yml's identifying the owning organization and trustee organizations
4+
# dashboard-accessor-*-config.yml in the configuration path,
5+
# with the yml's identifying the owning organization and accessor organizations
66
# respectively.
77
#
88
# You can also specify the configurations under this section too,
9-
# with keys owning and trustees.
9+
# with keys owning and accessors.
1010
#
1111
# Examples:
1212
# owning: example.dashboard-config.yml
13-
# trustees:
13+
# accessors:
1414
# org1: example.dashboard-config.yml
1515

16-
# specifies the filename format for the trustee org configurations.
16+
# specifies the filename format for the accessor org configurations.
1717
# a filename that matches the format will have the organization name extracted
1818
# from the filename. Default is:
19-
# trustee_config_filename_format: "dashboard-trustee-{organization_name}-config.yml"
19+
# accessor_config_filename_format: "dashboard-accessor-{organization_name}-config.yml"
2020

2121
directory:
2222
# (optional) Default country code to use if directory doesn't provide one for a user [Must be two-letter ISO-3166 code - see https://en.wikipedia.org/wiki/ISO_3166-1]
@@ -45,7 +45,7 @@ directory:
4545
# dashboard_groups: a list of strings identifying the dashboard groups.
4646
#
4747
# a group in dashboard_groups can be qualified with, the first part being
48-
# the trustee organization name.
48+
# the accessor organization name.
4949
# e.g. org1::Default Acrobat Pro DC configuration
5050
#
5151
# examples:
@@ -66,6 +66,10 @@ directory:
6666
# Default is:
6767
# user_identity_type: enterpriseID
6868

69+
limits:
70+
max_deletions_per_run: 10 # if --remove-nonexistent-users is specified, this is the most users that will be removed. Others will be left for a later run. A critical message will be logged.
71+
max_missing_users: 200 # if more than this number of user accounts are not found in the directory, user sync will abort with an error and a critical message will be logged.
72+
6973
logging:
7074
# specifies whether you wish to generate a log file
7175
# 'True' or 'False'
Binary file not shown.
Binary file not shown.

tests/config_test.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ def test_get_directory_connector_options(self, mock_dict, mock_connector_conf):
5555
@mock.patch('user_sync.config.ConfigLoader.create_dashboard_options')
5656
@mock.patch('glob.glob1')
5757
@mock.patch('user_sync.config.ConfigLoader.parse_string')
58-
def test_get_dashboard_options_for_trustees(self, mock_parse, mock_glob, mock_create_dash, mock_get_dict):
58+
def test_get_dashboard_options_for_accessors(self, mock_parse, mock_glob, mock_create_dash, mock_get_dict):
5959
mock_create_dash.return_value = {'create_dash'}
6060
mock_glob.return_value = {''}
6161
mock_parse.return_value = {'organization_name': 'testOrgName'}
6262

63-
self.assertEquals(self.conf_load.get_dashboard_options_for_trustees(), {'testOrgName': set(['create_dash'])},
64-
'We return with trustee option in the expected format')
63+
self.assertEquals(self.conf_load.get_dashboard_options_for_accessors(), {'testOrgName': set(['create_dash'])},
64+
'We return with accessor option in the expected format')
6565
self.assertEquals(mock_create_dash.call_count, 1, 'create dashboard options was called')
6666

6767
def test_get_dict_from_sources_dict(self):
@@ -88,19 +88,26 @@ def test_create_dashboard_options(self, mock_dict):
8888

8989
@mock.patch('user_sync.config.DictConfig.get_string')
9090
@mock.patch('user_sync.config.DictConfig.get_dict_config')
91+
@mock.patch('user_sync.config.DictConfig.get_list_config')
9192
@mock.patch('user_sync.identity_type.parse_identity_type')
92-
def test_get_rule_options(self, mock_id_type,mock_get_dict,mock_get_string):
93+
def test_get_rule_options(self, mock_id_type,mock_get_dict,mock_get_list,mock_get_string):
9394
mock_id_type.return_value = 'new_acc'
9495
mock_get_dict.return_value = tests.helper.MockGetString()
96+
mock_get_list.return_value = tests.helper.MockGetString()
9597
self.assertEquals(self.conf_load.get_rule_options(), {'username_filter_regex': None,
9698
'update_user_info': True,
9799
'manage_groups': True,
100+
'max_deletions_per_run': 1,
101+
'max_missing_users': 1,
98102
'new_account_type': 'new_acc',
99103
'directory_group_filter': None,
100104
'default_country_code': 'test',
101105
'remove_user_key_list': None,
102106
'remove_list_output_path': None,
103-
'remove_nonexistent_users': False},
107+
'remove_nonexistent_users': False,
108+
'after_mapping_hook': None,
109+
'extended_attributes': None,
110+
},
104111
'rule options are returned')
105112

106113
def test_parse_string(self):

tests/helper.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,10 @@ def create_action_manager():
8585

8686
class MockGetString():
8787
def get_string(self,test1,test2):
88-
return 'test'
88+
return 'test'
89+
90+
def get_int(self,test1):
91+
return 1
92+
93+
def iter_dict_configs(self):
94+
return iter([])

tests/rules_test.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ class RulesTest(unittest.TestCase):
3030

3131
def test_normal(self):
3232
owning_organization_name = user_sync.rules.OWNING_ORGANIZATION_NAME
33-
trustee_1_organization_name = "trustee1"
33+
accessor_1_organization_name = "accessor1"
3434
directory_group_1 = 'acrobat1'
3535
directory_group_2 = 'acrobat2'
3636
owning_group_11 = 'acrobat11'
3737
owning_group_12 = 'acrobat12'
3838
owning_group_21 = 'acrobat21'
3939
directory_groups = {
40-
directory_group_1: [user_sync.rules.Group(owning_group_11, owning_organization_name), user_sync.rules.Group('acrobat12', trustee_1_organization_name)],
40+
directory_group_1: [user_sync.rules.Group(owning_group_11, owning_organization_name), user_sync.rules.Group('acrobat12', accessor_1_organization_name)],
4141
directory_group_2: [user_sync.rules.Group(owning_group_21, owning_organization_name)]
4242
}
4343
all_users = [tests.helper.create_test_user([directory_group_1]),
@@ -53,19 +53,19 @@ def test_normal(self):
5353
owning_user_1['groups'] = [owning_group_11]
5454
owning_users.append(owning_user_1)
5555

56-
def mock_load_users_and_groups(groups):
56+
def mock_load_users_and_groups(groups, extended_attributes=None):
5757
return (True, list(all_users))
5858
mock_directory_connector = mock.mock.create_autospec(user_sync.connector.directory.DirectoryConnector)
5959
mock_directory_connector.load_users_and_groups = mock_load_users_and_groups
6060

6161
owning_commands_list = []
6262
mock_owning_dashboard_connector = self.create_mock_dashboard_connector(owning_users, owning_commands_list)
6363

64-
trustee_commands_list = []
65-
mock_trustee_dashboard_connector = self.create_mock_dashboard_connector([], trustee_commands_list)
64+
accessor_commands_list = []
65+
mock_accessor_dashboard_connector = self.create_mock_dashboard_connector([], accessor_commands_list)
6666

6767
dashboard_connectors = user_sync.rules.DashboardConnectors(mock_owning_dashboard_connector, {
68-
trustee_1_organization_name: mock_trustee_dashboard_connector
68+
accessor_1_organization_name: mock_accessor_dashboard_connector
6969
})
7070

7171
rule_processor = user_sync.rules.RuleProcessor({})
@@ -92,18 +92,18 @@ def mock_load_users_and_groups(groups):
9292
commands.add_user(self.create_user_attributes_for_commands(user, rule_options['update_user_info']))
9393
expected_owning_commands_list.append(commands)
9494

95-
expected_trustee_commands_list = []
95+
expected_accessor_commands_list = []
9696
user = all_users[0]
9797
commands = tests.helper.create_dashboard_commands(user)
9898
commands.add_groups(set([owning_group_12]))
99-
expected_trustee_commands_list.append(commands)
99+
expected_accessor_commands_list.append(commands)
100100

101101
tests.helper.assert_equal_dashboard_commands_list(self, expected_owning_commands_list, owning_commands_list)
102-
tests.helper.assert_equal_dashboard_commands_list(self, expected_trustee_commands_list, trustee_commands_list)
102+
tests.helper.assert_equal_dashboard_commands_list(self, expected_accessor_commands_list, accessor_commands_list)
103103

104104
# default country code tests
105-
106-
def _do_country_code_test(self, mock_dashboard_commands, mock_connectors, identity_type, default_country_code, user_country_code, expected_country_code):
105+
@mock.patch('logging.getLogger')
106+
def _do_country_code_test(self, mock_dashboard_commands, mock_connectors, identity_type, default_country_code, user_country_code, expected_country_code, mock_logger):
107107
expected_result = {'lastname': 'User1', 'email': '[email protected]', 'firstname': '!Openldap CCE', 'option': 'updateIfAlreadyExists'}
108108
if (expected_country_code):
109109
expected_result['country'] = expected_country_code
@@ -121,7 +121,11 @@ def _do_country_code_test(self, mock_dashboard_commands, mock_connectors, identi
121121
'uid': '001'}
122122
}
123123
mock_rules.add_dashboard_user('[email protected]', mock_connectors)
124-
mock_dashboard_commands.return_value.add_user.assert_called_with(expected_result)
124+
125+
if (identity_type == 'federatedID' and default_country_code == None and user_country_code == None):
126+
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.','[email protected]')
127+
else:
128+
mock_dashboard_commands.return_value.add_user.assert_called_with(expected_result)
125129

126130
# federatedId
127131
@mock.patch('user_sync.rules.DashboardConnectors')

user_sync/app.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ def init_log(logging_config):
9292
builder = user_sync.config.OptionsBuilder(logging_config)
9393
builder.set_bool_value('log_to_file', False)
9494
builder.set_string_value('file_log_directory', 'logs')
95-
builder.set_string_value('file_log_level', 'debug')
96-
builder.set_string_value('console_log_level', None)
95+
builder.set_string_value('file_log_level', 'info')
96+
builder.set_string_value('console_log_level', 'info')
9797
options = builder.get_options()
9898

9999
level_lookup = {
@@ -105,11 +105,18 @@ def init_log(logging_config):
105105
}
106106

107107
console_log_level = level_lookup.get(options['console_log_level'])
108-
if (console_log_level != None):
109-
console_log_handler.setLevel(console_log_level)
110-
108+
if (console_log_level == None):
109+
console_log_level = logging.INFO
110+
logger.log(logging.WARNING, 'Unknown console log level: %s setting to info' % options['console_log_level'])
111+
console_log_handler.setLevel(console_log_level)
112+
113+
111114
if options['log_to_file'] == True:
112-
file_log_level = level_lookup.get(options['file_log_level'], logging.NOTSET)
115+
unknown_file_log_level = False
116+
file_log_level = level_lookup.get(options['file_log_level'])
117+
if (file_log_level == None):
118+
file_log_level = logging.INFO
119+
unknown_file_log_level = True
113120
file_log_directory = options['file_log_directory']
114121
if not os.path.exists(file_log_directory):
115122
os.makedirs(file_log_directory)
@@ -119,6 +126,8 @@ def init_log(logging_config):
119126
fileHandler.setLevel(file_log_level)
120127
fileHandler.setFormatter(logging.Formatter(LOG_STRING_FORMAT, LOG_DATE_FORMAT))
121128
logging.getLogger().addHandler(fileHandler)
129+
if (unknown_file_log_level == True):
130+
logger.log(logging.WARNING, 'Unknown file log level: %s setting to info' % options['file_log_level'])
122131

123132
def begin_work(config_loader):
124133
'''
@@ -127,7 +136,7 @@ def begin_work(config_loader):
127136

128137
directory_groups = config_loader.get_directory_groups()
129138
owning_dashboard_config = config_loader.get_dashboard_options_for_owning()
130-
trustee_dashboard_configs = config_loader.get_dashboard_options_for_trustees()
139+
accessor_dashboard_configs = config_loader.get_dashboard_options_for_accessors()
131140
rule_config = config_loader.get_rule_options()
132141

133142
referenced_organization_names = set()
@@ -136,10 +145,10 @@ def begin_work(config_loader):
136145
organization_name = group.organization_name
137146
if (organization_name != user_sync.rules.OWNING_ORGANIZATION_NAME):
138147
referenced_organization_names.add(organization_name)
139-
referenced_organization_names.difference_update(trustee_dashboard_configs.iterkeys())
148+
referenced_organization_names.difference_update(accessor_dashboard_configs.iterkeys())
140149

141150
if (len(referenced_organization_names) > 0):
142-
raise user_sync.error.AssertionException('dashboard_groups have references to unknown trustee dashboards: %s' % referenced_organization_names)
151+
raise user_sync.error.AssertionException('dashboard_groups have references to unknown accessor dashboards: %s' % referenced_organization_names)
143152

144153
directory_connector = None
145154
directory_connector_options = None
@@ -155,11 +164,11 @@ def begin_work(config_loader):
155164
directory_connector.initialize(directory_connector_options)
156165

157166
dashboard_owning_connector = user_sync.connector.dashboard.DashboardConnector("owning", owning_dashboard_config)
158-
dashboard_trustee_connectors = {}
159-
for trustee_organization_name, trustee_config in trustee_dashboard_configs.iteritems():
160-
dashboard_trustee_conector = user_sync.connector.dashboard.DashboardConnector("trustee.%s" % trustee_organization_name, trustee_config)
161-
dashboard_trustee_connectors[trustee_organization_name] = dashboard_trustee_conector
162-
dashboard_connectors = user_sync.rules.DashboardConnectors(dashboard_owning_connector, dashboard_trustee_connectors)
167+
dashboard_accessor_connectors = {}
168+
for accessor_organization_name, accessor_config in accessor_dashboard_configs.iteritems():
169+
dashboard_accessor_conector = user_sync.connector.dashboard.DashboardConnector("accessor.%s" % accessor_organization_name, accessor_config)
170+
dashboard_accessor_connectors[accessor_organization_name] = dashboard_accessor_conector
171+
dashboard_connectors = user_sync.rules.DashboardConnectors(dashboard_owning_connector, dashboard_accessor_connectors)
163172

164173
rule_processor = user_sync.rules.RuleProcessor(rule_config)
165174
if (len(directory_groups) == 0 and rule_processor.will_manage_groups()):
@@ -266,7 +275,7 @@ def main():
266275

267276
except user_sync.error.AssertionException as e:
268277
if (not e.is_reported()):
269-
logger.error(e.message)
278+
logger.critical(e.message)
270279
e.set_reported()
271280
except:
272281
try:

0 commit comments

Comments
 (0)