Skip to content

Commit 4b6dfe6

Browse files
committed
Merge branch 'v2' into v2-sign-phase-2
2 parents 0e04abf + a25b172 commit 4b6dfe6

File tree

4 files changed

+120
-24
lines changed

4 files changed

+120
-24
lines changed

docs/en/user-manual/advanced_configuration.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,44 @@ group name. Join them with "::". For example:
169169
- "d37::Special Ops Group"
170170
```
171171
172+
### ESM Secondary Targets
173+
174+
Organizations with Enterprise Storage Model (ESM) enabled handle users differently from traditional User Storage Model (USM) organizations.
175+
ESM organizations use Business IDs instead of Adobe IDs. Business IDs are special accounts that govern a user's profile for a specific
176+
organization. Business IDs do not directly provide authentication capabilities for users. Instead, each is linked to an Adobe ID,
177+
Enterprise ID or Federated ID that handles authentication.
178+
179+
The nature of Business IDs creates unique behavior in ESM organizations that have a trust with an existing user directory. Users
180+
with a domain belonging to the directory will always be created as Business ID instead of the actual user type (Federated or Enterprise,
181+
depending on directory settings).
182+
183+
For example, if we have a Federated directory with a claim on `example.com`, and we create a new user in the ESM trustee
184+
(e.g. `[email protected]`), then that user will be a Business ID in the trustee console and not Federate. The user will
185+
still use the configured Identity Provider to authenticate.
186+
187+
When syncing to secondary ESM targets, this feature prevents the UST from fully managing Business ID users on trustee consoles.
188+
When performing user sync on secondary targets, the UST expects the identity types between users on parent and child to match.
189+
190+
To manage Business IDs as their linked identity type, enable the `uses_business_id` option in the secondary target's UMAPI
191+
connector config file.
192+
193+
```yaml
194+
# connector-umapi-org2.yml
195+
server:
196+
host: usermanagement.adobe.io
197+
ims_host: ims-na1.adobelogin.com
198+
enterprise:
199+
org_id: xxx@AdobeOrg
200+
client_secret: xxx
201+
priv_key_path: private-org2.key
202+
client_id: xxx
203+
tech_acct_id: [email protected]
204+
uses_business_id: True
205+
```
206+
207+
This essentially overrides the Business ID user type to the type of the user from the primary target, ensuring that
208+
the full user lifecycle of the user on the secondary target is managed.
209+
172210
## Custom Attributes and Mappings
173211

174212
It is possible to define custom mappings of directory attribute

tests/test_config.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import pytest
22

33
import user_sync.engine.umapi
4+
import yaml
5+
import shutil
6+
from user_sync.connector.connector_umapi import UmapiConnector
7+
from user_sync.config import ConfigFileLoader, ConfigLoader, DictConfig
48
from user_sync import flags
59
from user_sync.config.common import ConfigFileLoader, DictConfig
610
from user_sync.config.user_sync import UMAPIConfigLoader
@@ -174,5 +178,45 @@ def test_shell_exec_flag(test_resources, modify_root_config, cli_args, monkeypat
174178

175179
args = cli_args({'config_filename': root_config_file})
176180
modify_root_config(['directory_users', 'connectors', 'ldap'], "$(some command)")
177-
with pytest.raises(AssertionException):
178-
UMAPIConfigLoader(args)
181+
config_loader = ConfigLoader(args)
182+
183+
directory_connector_module_name = config_loader.get_directory_connector_module_name()
184+
if directory_connector_module_name is not None:
185+
directory_connector = DirectoryConnector()
186+
with pytest.raises(AssertionException):
187+
config_loader.get_directory_connector_options(directory_connector.name)
188+
189+
190+
def test_uses_business_id_true(tmp_config_files, modify_umapi_config, cli_args, private_key):
191+
root_config, _, _ = tmp_config_files
192+
modify_umapi_config(['uses_business_id'], True)
193+
modify_umapi_config(['enterprise', 'priv_key_path'], private_key)
194+
args = cli_args({'config_filename': root_config})
195+
config_loader = ConfigLoader(args)
196+
connector_options, _ = config_loader.get_umapi_options()
197+
UmapiConnector.create_conn = False
198+
umapi_connector = UmapiConnector('.primary', connector_options)
199+
assert umapi_connector.uses_business_id
200+
201+
202+
def test_uses_business_id_false(tmp_config_files, modify_umapi_config, cli_args, private_key):
203+
root_config, _, _ = tmp_config_files
204+
modify_umapi_config(['uses_business_id'], False)
205+
modify_umapi_config(['enterprise', 'priv_key_path'], private_key)
206+
args = cli_args({'config_filename': root_config})
207+
config_loader = ConfigLoader(args)
208+
connector_options, _ = config_loader.get_umapi_options()
209+
UmapiConnector.create_conn = False
210+
umapi_connector = UmapiConnector('.primary', connector_options)
211+
assert not umapi_connector.uses_business_id
212+
213+
214+
def test_uses_business_id_unspecified(tmp_config_files, modify_umapi_config, cli_args, private_key):
215+
root_config, _, _ = tmp_config_files
216+
modify_umapi_config(['enterprise', 'priv_key_path'], private_key)
217+
args = cli_args({'config_filename': root_config})
218+
config_loader = ConfigLoader(args)
219+
connector_options, _ = config_loader.get_umapi_options()
220+
UmapiConnector.create_conn = False
221+
umapi_connector = UmapiConnector('.primary', connector_options)
222+
assert not umapi_connector.uses_business_id

user_sync/connector/connector_umapi.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@
4444

4545

4646
class UmapiConnector(object):
47-
def __init__(self, name, caller_options):
47+
# class-level flag that determines if we are creating a UMAPI connection
48+
# set to False if using in a unit test
49+
create_conn = True
50+
def __init__(self, name, caller_options, is_primary=False):
4851
"""
4952
:type name: str
5053
:type caller_options: dict
@@ -54,6 +57,8 @@ def __init__(self, name, caller_options):
5457
self.trusted = caller_config.get_bool('trusted', True)
5558
if self.trusted is None:
5659
self.trusted = False
60+
self.uses_business_id = caller_config.get_bool('uses_business_id', True)
61+
self.is_primary = is_primary
5762
builder = config_common.OptionsBuilder(caller_config)
5863
builder.set_string_value('logger_name', self.name)
5964
builder.set_bool_value('test_mode', False)
@@ -95,26 +100,27 @@ def __init__(self, name, caller_options):
95100
enterprise_config.report_unused_values(logger)
96101
# open the connection
97102
um_endpoint = "https://" + server_options['host'] + server_options['endpoint']
98-
logger.debug('%s: creating connection for org %s at endpoint %s', self.name, org_id, um_endpoint)
99-
try:
100-
self.connection = connection = umapi_client.Connection(
101-
org_id=org_id,
102-
auth_dict=auth_dict,
103-
ims_host=ims_host,
104-
ims_endpoint_jwt=server_options['ims_endpoint_jwt'],
105-
user_management_endpoint=um_endpoint,
106-
test_mode=options['test_mode'],
107-
user_agent="user-sync/" + app_version,
108-
logger=self.logger,
109-
timeout_seconds=float(server_options['timeout']),
110-
retry_max_attempts=server_options['retries'] + 1,
111-
ssl_verify=options['ssl_cert_verify']
112-
)
113-
except Exception as e:
114-
raise AssertionException("Connection to org %s at endpoint %s failed: %s" % (org_id, um_endpoint, e))
115-
logger.debug('%s: connection established', self.name)
116-
# wrap the connection in an action manager
117-
self.action_manager = ActionManager(connection, org_id, logger)
103+
if self.create_conn:
104+
logger.debug('%s: creating connection for org %s at endpoint %s', self.name, org_id, um_endpoint)
105+
try:
106+
self.connection = connection = umapi_client.Connection(
107+
org_id=org_id,
108+
auth_dict=auth_dict,
109+
ims_host=ims_host,
110+
ims_endpoint_jwt=server_options['ims_endpoint_jwt'],
111+
user_management_endpoint=um_endpoint,
112+
test_mode=options['test_mode'],
113+
user_agent="user-sync/" + app_version,
114+
logger=self.logger,
115+
timeout_seconds=float(server_options['timeout']),
116+
retry_max_attempts=server_options['retries'] + 1,
117+
ssl_verify=options['ssl_cert_verify']
118+
)
119+
except Exception as e:
120+
raise AssertionException("Connection to org %s at endpoint %s failed: %s" % (org_id, um_endpoint, e))
121+
logger.debug('%s: connection established', self.name)
122+
# wrap the connection in an action manager
123+
self.action_manager = ActionManager(connection, org_id, logger)
118124

119125
def get_users(self):
120126
return list(self.iter_users())

user_sync/engine/umapi.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import user_sync.connector.connector_umapi
2828
import user_sync.error
2929
import user_sync.identity_type
30+
from user_sync.connector.connector_umapi import UmapiConnector
3031
from user_sync.helper import normalize_string, CSVAdapter, JobStats
3132
from user_sync.config.common import check_max_limit
3233

@@ -112,6 +113,7 @@ def __init__(self, caller_options):
112113
self.primary_users_created = set()
113114
self.secondary_users_created = set()
114115
self.updated_user_keys = set()
116+
self.primary_users_by_email: dict[str, dict] = {}
115117

116118
# stray key input path comes in, stray_list_output_path goes out
117119
self.stray_key_map = {}
@@ -882,7 +884,7 @@ def update_umapi_user(self, umapi_info, user_key, attributes_to_update=None, gro
882884
commands.add_groups(groups_to_add)
883885
return commands
884886

885-
def update_umapi_users_for_connector(self, umapi_info, umapi_connector):
887+
def update_umapi_users_for_connector(self, umapi_info, umapi_connector: UmapiConnector):
886888
"""
887889
This is the main function that goes over adobe users and looks for and processes differences.
888890
It is called with a particular organization that it should manage groups against.
@@ -920,6 +922,9 @@ def update_umapi_users_for_connector(self, umapi_info, umapi_connector):
920922
# Walk all the adobe users, getting their group data, matching them with directory users,
921923
# and adjusting their attribute and group data accordingly.
922924
for umapi_user in umapi_users:
925+
# if target is ESM, then treat existing AdobeID (i.e. businessID) as linked identity type
926+
if umapi_connector.uses_business_id and umapi_user['email'] in self.primary_users_by_email:
927+
umapi_user['type'] = self.primary_users_by_email[umapi_user['email']]['type']
923928
# let save adobeID users to a seperate list
924929
self.filter_adobeID_user(umapi_user)
925930
# get the basic data about this user; initialize change markers to "no change"
@@ -948,6 +953,9 @@ def update_umapi_users_for_connector(self, umapi_info, umapi_connector):
948953

949954
self.map_email_override(umapi_user)
950955

956+
if umapi_connector.is_primary:
957+
self.primary_users_by_email[umapi_user['email']] = umapi_user
958+
951959
directory_user = filtered_directory_user_by_user_key.get(user_key)
952960
if directory_user is None:
953961
# There's no selected directory user matching this adobe user

0 commit comments

Comments
 (0)