diff --git a/docs/success-guide/command_line_options.md b/docs/success-guide/command_line_options.md index 8b6dc0d8d..3b436f096 100644 --- a/docs/success-guide/command_line_options.md +++ b/docs/success-guide/command_line_options.md @@ -55,7 +55,7 @@ Note that only the users returned by the directory query and filter are consider `--test-mode`: causes User Sync to run through all processing including querying the directory and calling the Adobe User Management APIs to process the request, but no actual action is taken. No users are created, deleted, or altered. -`--update-user-info`: causes User Sync to check for changes in first name, last name, or email address of users and make updates to the Adobe information if it does not match the directory information. Specifying this option increases run time so you may not want to include it on each run. +`--update-user-info`: causes User Sync to check for changes in first name, last name, or email address of users and make updates to the Adobe information if it does not match the directory information. Specifying this option may increase run time. ## Examples diff --git a/docs/success-guide/images/install_config_files.png b/docs/success-guide/images/install_config_files.png index 8bbae6eba..2df4767fd 100644 Binary files a/docs/success-guide/images/install_config_files.png and b/docs/success-guide/images/install_config_files.png differ diff --git a/docs/success-guide/images/layout_orgs_multi_dir_single_org.png b/docs/success-guide/images/layout_orgs_multi_dir_single_org.png index 47a8e6cfe..6b443a2b2 100644 Binary files a/docs/success-guide/images/layout_orgs_multi_dir_single_org.png and b/docs/success-guide/images/layout_orgs_multi_dir_single_org.png differ diff --git a/docs/success-guide/images/layout_products_map.png b/docs/success-guide/images/layout_products_map.png index 87212abff..f48da4809 100644 Binary files a/docs/success-guide/images/layout_products_map.png and b/docs/success-guide/images/layout_products_map.png differ diff --git a/docs/success-guide/images/setup_config_group_map.png b/docs/success-guide/images/setup_config_group_map.png index a5c3fbac9..427f6c24b 100644 Binary files a/docs/success-guide/images/setup_config_group_map.png and b/docs/success-guide/images/setup_config_group_map.png differ diff --git a/docs/success-guide/images/test_run_screen.png b/docs/success-guide/images/test_run_screen.png index 85f198f05..152aa71c6 100644 Binary files a/docs/success-guide/images/test_run_screen.png and b/docs/success-guide/images/test_run_screen.png differ diff --git a/docs/success-guide/install_sync.md b/docs/success-guide/install_sync.md index 8cdbe8156..05acd6f25 100644 --- a/docs/success-guide/install_sync.md +++ b/docs/success-guide/install_sync.md @@ -32,11 +32,11 @@ Select “release” ![install2](images/install_release_screen.png) -☐ Download the examples.config.tar.gz, User Sync Guide, and build for your platform, osx, ubuntu, windows, or centos. +☐ Download the example-configurations.tar.gz, User Sync Guide, and build for your platform, osx, ubuntu, windows, or centos. ☐ Extract the user-sync (or user-sync.pex) file from the archive and place the file for your OS in the folder. In our example, this would be /home/user_sync/user_sync_tool/user-sync or C:\Users\user_sync\user_sync_tool\user-sync.pex. -☐ In the examples.config.tar.gz file there is a directory **config files - basic**. From this folder extract the first 3 files and place in the user_sync_tool folder. +☐ In the example-configurations.tar.gz file there is a directory **config files - basic**. From this folder extract the first 3 files and place in the user_sync_tool folder. ☐ Next, rename the 3 config example files by removing the leading "1 ", "2 ", and "3 " from the names. We will edit these files to create the real User Sync configuration files. diff --git a/docs/success-guide/layout_products.md b/docs/success-guide/layout_products.md index 9719a810a..e188f90da 100644 --- a/docs/success-guide/layout_products.md +++ b/docs/success-guide/layout_products.md @@ -21,11 +21,12 @@ You can use User Sync to manage PC membership and license allocation. This is o User Sync helps you manage Adobe product licenses by allowing you to place users into directory groups using the directory system interface or other tools. Those groups are then mapped to Adobe user groups or PCs. The mapping is part of the User Sync configuration file. When User Sync detects that directory users are in one of these mapped groups, the user is added to the corresponding Adobe user group or PC. Similarly, users in the user group or PC but not in the corresponding directory group are removed from the user group or PC. -☐ Decide if you will manage license allocation using User Sync. If not, you can skip the remaining steps for now, but you will still need to do them and manually add users to the user groups or PCs later using the Adobe Admin Console. +☐ Decide if you will manage license allocation using User Sync. If not, you can skip the remaining steps; however, before the users you create can access Adobe products, you will need to manually add them to product configurations using the Adobe Admin Console. -☐ Create the PCs in the Adobe Admin console for the configurations of products and groups of users you will be managing. +☐ Create the PCs in the Adobe Admin console for the product configurations and groups of users you will be managing. Add the comment "Managed by User Sync - do not edit" to the description field of the PC. + +☐ If you are going to use User Groups to manage product access, you will need to first create the user groups and the product configurations, then you can manage product access by adding each user group to the appropriate product configurations. Add the comment "Managed by User Sync - do not edit" to the description field of the user group. -☐ If you are going to use User Groups, create them and add them to the PC(s) representing product licenses to be issued to members of the user group. ☐ Draw a diagram of your Adobe organizations, and the products and PCs in each. Add the directory and directory groups to the picture and show the mapping. For example: diff --git a/docs/success-guide/scheduling.md b/docs/success-guide/scheduling.md index ab4b3ed05..d9e62bc45 100644 --- a/docs/success-guide/scheduling.md +++ b/docs/success-guide/scheduling.md @@ -12,7 +12,7 @@ layout: default First, create a batch file with the invocation of user-sync piped to a scan to pull out relevant log entries for a summary. Create the file run_sync.bat for this with contents like: cd user-sync-directory - python user-sync.pex --users file example.users-file.csv --process-groups | findstr "==== ----- WARNING ERROR CRITICAL" > temp.file.txt + python user-sync.pex --users file example.users-file.csv --process-groups | findstr /I "==== ----- WARNING ERROR CRITICAL Number" > temp.file.txt rem email the contents of temp.file.txt to the user sync administration your-mail-tool –send file temp.file.txt @@ -34,7 +34,8 @@ Note that often when setting up scheduled tasks, commands that work from the com First, create a shell script with the invocation of user-sync piped to a scan to pull out relevant log entries for a summary. Create the file run_sync.sh for this with contents like: cd user-sync-directory - ./user-sync --users file example.users-file.csv --process-groups | grep "CRITICAL\\|WARNING\\|ERROR\\|=====\\|-----" | mail -s “Adobe User Sync Report for `date +%F-%a`” Your_admin_mailing_list@example.com + ./user-sync --users file example.users-file.csv --process-groups | grep "CRITICAL\\|WARNING\\|ERROR\\|=====\\|-----\\|number of\\|Number of" | mail -s “Adobe User Sync Report for `date +%F-%a`” + Your_admin_mailing_list@example.com You need to fill in your specific User Sync command line options and the email address to which the report should be sent. diff --git a/docs/success-guide/setup_adobeio.md b/docs/success-guide/setup_adobeio.md index eeb0a50b5..c0df1ffbd 100644 --- a/docs/success-guide/setup_adobeio.md +++ b/docs/success-guide/setup_adobeio.md @@ -18,7 +18,7 @@ Setup steps are documented. For complete information about the integration setu - Once setup, the Adobe.io console displays all needed values. You’ll copy these into the user sync configuration file. - You'll also need to add the private key file to the User Sync configuration. -☐ Obtain or create a digital signing certificate. See [insteuctions for certificate creation](https://www.adobe.io/apis/cloudplatform/usermanagement/docs/setup/createcert.html). +☐ Obtain or create a digital signing certificate. See [instructions for certificate creation](https://www.adobe.io/apis/cloudplatform/usermanagement/docs/setup/createcert.html). ☐ Setup an adobe.io integration for each organization you need to access (usually only one). See Step 2 and 3 on this [page](https://www.adobe.io/apis/cloudplatform/usermanagement/docs/setup.html) diff --git a/docs/success-guide/setup_config_files.md b/docs/success-guide/setup_config_files.md index f8daaa2c7..ee450cab7 100644 --- a/docs/success-guide/setup_config_files.md +++ b/docs/success-guide/setup_config_files.md @@ -28,7 +28,7 @@ In previous steps, you set up a file system directory for the user sync tool Pyt ### Directory Access Configuration File -If you are driving User Sync from a file, you can skip this step. Setup a csv file with your entire user list following the "csv inputs - user and remove lists/1 users-file.csv" file example. This file is in the examples.config.tar.gz download from the release. +If you are driving User Sync from a file, you can skip setting up connector-ldap.yml and instead create a csv file with your entire user list following the "csv inputs - user and remove lists/1 users-file.csv" file example. This file is in the example-configurations.tar.gz download from the release. ☐ Edit the file connector-ldap.yml. This file has access information to the directory system. Put in username, password, host, and base_dn values. @@ -43,7 +43,7 @@ If you need a non-default LDAP query to select the desired set of users, it is s ☐ Edit the connector-umapi.yml. Put in the information from the adobe.io integration you created earlier. This would be the org\_id, api\_key, client\_secret, and tech\_acct. -☐ Place the private key file in the user_sync_tool folder The priv\_key\_path config file items is then set to the name of this file. +☐ Place the private key file in the user_sync_tool folder. The priv\_key\_path config file item is then set to the name of this file. ![](images/setup_config_umapi.png) @@ -59,7 +59,8 @@ Edit the user-sync-config.yml file. # example: # default_country_code: US -If your directory does not list a country for each user, you can set a default country here. Remove the "# " from the default country code line so it looks like this + +☐ If your directory does not list a country for each user, you can set a default country here. Remove the "# " from the default country code line so it looks like this default_country_code: US @@ -103,7 +104,7 @@ A more realistic example is: groups: - directory_group: acrobat_pro_dc adobe_groups: - - Default Acrobat Pro DC configuration + - Default Acrobat_Users - directory_group: all_apps adobe_groups: - All Apps @@ -116,10 +117,10 @@ A more realistic example is: Limits on deletion prevent accidental account deletion in the event of misconfiguration or some other problem that results in User Sync not getting proper data from the directory system. -☐ If you expect the number of directory users to drop by more than 200 between User Sync runs, then you will need to raise the `max_adobe_only_users` value. These config file entries are to prevent runaway deletion in case of misconfiguration or other problems. +☐ If you expect the number of directory users to drop by more than 200 between User Sync runs, then you will need to raise the `max_adobe_only_users` value. This config file entry prevents runaway deletion in case of misconfiguration or other problems. limits: -     max_adobe_only_users: 200      # abort if this many directory users disappear +     max_adobe_only_users: 200      # abort updates if this many directory users disappear @@ -127,11 +128,11 @@ Limits on deletion prevent accidental account deletion in the event of misconfig If you want to drive account creation and removal through User Sync, and want to manually create a few accounts then you may need this feature to keep User Sync from deleting your manually created accounts. -☐ If you need to use this feature, add lines such as below to the config file at the top level. To protect users on the Admin Console from updates, create a user group and put the protected users into that group, then list that group as excluded from User Sync processing. You can also list specific users and/or a pattern that matches specific user names to protect those users. You can protect users based on their identity type as well. For example, often User Sync is used only to manage federatedID or enterpriseID user types and you can exclude adobeID type users from management by User Sync. You only need to include configuration items for exclusions that you wish to use. +☐ If you need to use this feature, add lines such as below to the config file under adobe_users. To protect users on the Admin Console from updates, create a user group and put the protected users into that group, then list that group as excluded from User Sync processing. You can also list specific users and/or a pattern that matches specific user names to protect those users. You can protect users based on their identity type as well. For example, often User Sync is used only to manage federatedID or enterpriseID user types and you can exclude adobeID type users from management by User Sync. You only need to include configuration items for exclusions that you wish to use. ```YAML adobe_users: -  exclude_groups: +  exclude_adobe_groups: - administrators   # Names an Adobe user group or product configuration whose members are not to be altered or removed by User Sync - contractors # You can have more than one group in a list exclude_users: @@ -156,9 +157,10 @@ Note that: - Directory side users are still created on the Adobe side even if one of the exclude parameters would then exclude the user on the Adobe side from updates in successive runs. That is, these parameters apply only to Adobe users who exist when the Adobe directory is being matched against the customer directory. -- Accounts that would have been removed or updated but were not because of this feature are listed as `info` level log entries. +- Accounts that would have been removed or updated but were not because of this feature are listed as `debug` level log entries. - Federated accounts that are not in the directory or are disabled in the directory cannot log in anyway (because login is handled by the ID provider and the user is no longer listed there) even if the account still exists in Adobe. +- You will likely want to exclude identities of type adobeID because they are usually not listed in the enterprise directory. @@ -170,7 +172,7 @@ log\_to\_file turns the file log on or off. Messages can be on one of 5 level of importance and you can choose the lowest importance that will be included for either the file log or standard output log to the console. The defaults are to produce the file log and to include messages of level "info" or higher. This is the recommended setting. -☐ Review the settings for logs and make any desired changes. +☐ Review the settings for logs and make any desired changes. The recommended log level is info (which is the default). logging: # specifies whether you wish to generate a log file diff --git a/docs/success-guide/test_run.md b/docs/success-guide/test_run.md index d0433ae28..48f87b147 100644 --- a/docs/success-guide/test_run.md +++ b/docs/success-guide/test_run.md @@ -26,19 +26,19 @@ Give it a try: ☐ Next, try a sync limited to a single user and run in test mode. You need to know the name of some user in your directory. For example, if the user is bart@example.com, try: - ./user-sync -t --users all --user-filter bart@example.com + ./user-sync -t --users all --user-filter bart@example.com --adobe-only-user-action exclude - ./user-sync -t --users all --user-filter bart@example.com --process-groups + ./user-sync -t --users all --user-filter bart@example.com --process-groups --adobe-only-user-action exclude -The first command above will sync only the one user (because of the user filter) which should result in an attempt to create the user. Because of running in test mode (-t), the run of user-sync will only attempt to create the user and not actually do it. +The first command above will sync only the one user (because of the user filter) which should result in an attempt to create the user. Because of running in test mode (-t), the run of user-sync will only attempt to create the user and not actually do it. The `--adobe-only-user-action exclude` option will prevent updates to any user accounts that already exist in the Adobe organization. The second command above (with the --process-groups option) will attempt to create the user and add them to any groups that are mapped from the their directory groups. Again, this is in test mode so no actual action will be taken. If there are already existing users and the groups have users already added to them, user-sync may attempt to remove them. If this is the case, skip the next test. Also, if you are not using directory groups to manage product access, skip the tests that involve --process-groups. ☐ Next, try a sync limited to a single user and don't run in test mode. This should actually create the user and add to groups (if mapped). - ./user-sync --users all --user-filter bart@example.com --process-groups + ./user-sync --users all --user-filter bart@example.com --process-groups --adobe-only-user-action exclude - ./user-sync --users all --user-filter bart@example.com --process-groups + ./user-sync --users all --user-filter bart@example.com --process-groups --adobe-only-user-action exclude ☐ Next, go check on the Adobe Admin Console if the user has appeared and the group memberships have been added. diff --git a/docs/user-manual/index.md b/docs/user-manual/index.md index 106fa95e4..6515490b2 100644 --- a/docs/user-manual/index.md +++ b/docs/user-manual/index.md @@ -198,6 +198,8 @@ mapping in the main User Sync configuration file. See details in the [Configure group mapping](#configure-group-mapping) section below. +It is a best practice to note in the description field of the Product Configuration or User Group that the group is managed by User Sync and should not be edited in the Admin Console. + ![Figure 2: Group Mapping Overview](media/group-mapping.png) ### Installing the User Sync tool @@ -247,7 +249,7 @@ latest source off the master branch.) `user-sync.pex` for Windows) and place it in your User Sync folder. -4. Download the `examples.tar.gz` archive of sample configuration +4. Download the `example-configurations.tar.gz` archive of sample configuration files. Within the archive, there is a folder for “config files – basic”. The first 3 files in this folder are required. Other files in the package are optional and/or alternate versions for @@ -364,7 +366,7 @@ folder as the command-line executable. | Configuration File | Purpose | |:------|:---------| | user-sync-config.yml | Required. Contains configuration options that define the mapping of directory groups to Adobe product configurations and user groups, and that control the update behavior. Also contains references to the other config files.| -| adobe‑user‑config.yml   | Required. Contains credentials and access information for calling the Adobe User Management API. | +| connector‑umapi.yml   | Required. Contains credentials and access information for calling the Adobe User Management API. | | connector-ldap.yml | Required. Contains credentials and access information for accessing the enterprise directory. | @@ -378,11 +380,11 @@ below. Examples of the three required files are provided in the `config files - basic` folder in the release artifact -`example.config.files.tar.gz`: +`example-configurations.tar.gz`: ```text 1 user-sync-config.yml -2 adobe-user-config.yml +2 connector-umapi.yml 3 connector-ldap.yml ``` @@ -756,9 +758,9 @@ you can see the result before running live. 2. Add users to one or more configured directory/security groups. -3. Run User Sync in test mode. (`./user-sync -t --users all --process-groups`) +3. Run User Sync in test mode. (`./user-sync -t --users all --process-groups --adobe-only-user-action exclude`) -3. Run User Sync not in test mode. (`./user-sync --users all --process-groups`) +3. Run User Sync not in test mode. (`./user-sync --users all --process-groups --adobe-only-user-action exclude`) 4. Check that test users were created in Adobe Admin Console. @@ -766,7 +768,7 @@ you can see the result before running live. 1. Modify group membership of one or more test user in the directory. -1. Run User Sync. (`./user-sync -t --users all --process-groups`) +1. Run User Sync. (`./user-sync -t --users all --process-groups --adobe-only-user-action exclude`) 2. Check that test users in Adobe Admin Console were updated to reflect new product configuration membership. @@ -776,12 +778,12 @@ reflect new product configuration membership. 1. Remove or disable one or more existing test users in your enterprise directory. -2. Run User Sync. (`./user-sync -t --users all --process-groups`) +2. Run User Sync. (`./user-sync -t --users all --process-groups --adobe-only-user-action exclude`) 3. Check that users were removed from configured product configurations in the Adobe Admin Console. -4. Run User Sync to remove the users (`./user-sync -t --users all --process-groups --adobe-only-user-action delete`) Then run without -t. +4. Run User Sync to remove the users (`./user-sync -t --users all --process-groups --adobe-only-user-action delete`) Then run without -t. Caution: check that only the desired user was removed when running with -t. This run (without -t) will actually delete users. 5. Check that the user accounts are removed from the Adobe Admin Console. @@ -988,8 +990,8 @@ are flagged for removal to a CSV file. To perform the removals in a separate call, you can pass the file generated by the `--adobe-only-user-action write-file` parameter, or you can pass a CSV file of users that you have generated by some -other means. An example of such a file, example.users-file.csv, -is provided with the tool. +other means. An example of such a file, `3 remove-list.csv`, +is provided in the example-configurations.tar.gz file in the `csv inputs - user and remove lists` folder. ##### Add users and generate a list of users to remove @@ -1036,6 +1038,40 @@ on the list generated in a prior run of User Sync. ./user-sync --adobe-only-user-list users-to-delete.csv --adobe-only-user-action delete ``` +### Handling Push Notifications + +If your directory system can generate notifications of updates you can use User Sync to +process those updates incrementally. The technique shown in this section can also be +used to process immediate updates where an administrator has updated a user or group of +users and wants to push just those updates immediately into Adobe's user management +system. Some scripting may be required to transform the information coming from the +push notification to a csv format suitable for input to User Sync, and to separate +deletions from other updates, which mush be handled separately in User Sync. + +Create a file, say, `updated_users.csv` with the user update format illustrated in +the `users-file.csv` example file in the folder `csv inputs - user and remove lists`. +This is a basic csv file with columns for firstname, lastname, and so on. + + firstname,lastname,email,country,groups,type,username,domain + John,Smith,jsmith@example.com,US,"AdobeCC-All",enterpriseID + Jane,Doe,jdoe@example.com,US,"AdobeCC-All",federatedID + +This file is then provided to User Sync: + +```sh +./user-sync --users file updated-users.csv --process-groups --update-users --adobe-only-user-action exclude +``` + +The --adobe-only-user-action exclude causes User Sync to update only users that are in the updated-users.csv file and to ignore all others. + +Deletions are handled similarly. Create a file `deleted-users.csv` based on the format of `remove-list.csv` in the same example folder and run User Sync: + +```sh +./user-sync --adobe-only-user-list deleted-users.csv --adobe-only-user-action remove +``` + +This will handle deletions based on the notification and no other actions will be taken. Note that `remove` could be replaced with one of the other actions based on how you want to handle deleted users. + ### Action Summary At the end of the invocation, an action summary will be printed to the log (if the level is INFO or DEBUG). @@ -1591,7 +1627,7 @@ The following example shows how to set up a batch file `run_sync.bat` in Windows. ```sh -python C:\\...\\user-sync.pex --users file example.users-file.csv --process-groups | findstr "WARNING ERROR CRITICAL ---- ====" > temp.file.txt +python C:\\...\\user-sync.pex --users file users-file.csv --process-groups | findstr /I "WARNING ERROR CRITICAL ---- ==== Number" > temp.file.txt rem email the contents of temp.file.txt to the user sync administration sendmail -s “Adobe User Sync Report for today” UserSyncAdmins@example.com < temp.file.txt ``` @@ -1606,7 +1642,7 @@ The following example shows how to set up a shell file `run_sync.sh` on Linux or Mac OS X: ```sh -user-sync --users file example.users-file.csv --process-groups | grep "CRITICAL\|WARNING\|ERROR\|=====\|-----\|number of\|Number of" | mail -s “Adobe User Sync Report for `date +%F-%a`” UserSyncAdmins@example.com +user-sync --users file users-file.csv --process-groups | grep "CRITICAL\|WARNING\|ERROR\|=====\|-----\|number of\|Number of" | mail -s “Adobe User Sync Report for `date +%F-%a`” UserSyncAdmins@example.com ``` #### Schedule a UserSync task diff --git a/docs/user-manual/media/group-mapping.png b/docs/user-manual/media/group-mapping.png index a5c3fbac9..427f6c24b 100644 Binary files a/docs/user-manual/media/group-mapping.png and b/docs/user-manual/media/group-mapping.png differ diff --git a/examples/config files - basic/1 user-sync-config.yml b/examples/config files - basic/1 user-sync-config.yml index 4a6ce3cc6..af05011a5 100644 --- a/examples/config files - basic/1 user-sync-config.yml +++ b/examples/config files - basic/1 user-sync-config.yml @@ -20,7 +20,7 @@ adobe_users: # (optional) exclude_identity_types (no default value) # exclude_identity_types is a list of values, each of which must be # one of "adobeID", "enterpriseID", or "federatedID". Any Adobe-side - # user who has one of these identity types listed in this setting + # user who has one of the identity types listed in this setting # is excluded from updates by User Sync. [NOTE: It is highly # recommended that you exclude any identity types that a specific # User Sync job is not meant to manage. For example, a job that is @@ -29,7 +29,7 @@ adobe_users: # removed because your directory doesn't contain them.] exclude_identity_types: - adobeID - # + # (optional) exclude_adobe_groups (no default value) # exclude_adobe_groups is a list of values, each of which is a string # that names a product configuration or user group on the Adobe side. @@ -38,6 +38,7 @@ adobe_users: exclude_adobe_groups: #- "Sample Product Configuration" #- "Sample User Group" + # (optional) exclude_users (no default value) # exclude_users is a list of values, each of which is a Python # regular expression. Users who match any of these regular expressions @@ -53,6 +54,7 @@ adobe_users: # (required) connectors # The connectors section specifies how to connect User Sync to Adobe. connectors: + # (required) umapi # umapi stands for User Management API, and is the protocol # used to read and write users on the Adobe side. The value @@ -64,12 +66,12 @@ adobe_users: # working directory of your User Sync process. umapi: "connector-umapi.yml" -# The directory_users section controls how customer-side users are accessed, -# sets default values for attributes not specified in the customer directory, -# and also determines how customer-side directory groups correspond to +# The directory_users section controls how enterprise-side users are accessed, +# sets default values for attributes not specified in the enterprise directory, +# and also determines how enterprise-side directory groups correspond to # Adobe-side product configurations and user groups. directory_users: - # + # (optional) user_identity_type (default value enterpriseID) # All Adobe users have an identity type: one of Adobe ID, Enterprise ID, # or Federated ID. When a directory user is created on the Adobe side, @@ -77,23 +79,23 @@ directory_users: # identity type then determines whether the account is controlled by the # user (Adobe ID) or by the company (Enterprise ID or Federated ID), and # whether the sign-in process is handled by Adobe (Adobe ID or Enterprise ID) - # or by the customer Identity Provider (Federated ID). - # If your customer directory does not specify the Adobe-side identity type + # or by your Identity Provider (Federated ID). + # If your directory does not specify the Adobe-side identity type # for one (or any) of your users, you can specify a default type here that # will be used: one of "adobeID", "enterpriseID", or "federatedID". user_identity_type: enterpriseID - # + # (optional) default_country_code (no default value) # All Adobe users have a country code, which is a two-letter (ISO-3166) country code # which represents the home country of the user. - # If your customer directory doesn't have an appropriate value for each of your users, + # If your directory doesn't have an appropriate value for each of your users, # you can configure a default value here that applies to any user without one. # [NOTE: For Enterprise ID users, specifying a country code is not absolutely required # when they are created on the Adobe side. If none is specified, Adobe will ask # the user for his home country at the time of first sign-in. But to avoid mistakes, # it is highly recommended that IT assign the value via User Sync.] default_country_code: US - # + # (optional) extension (no default value) # Extensions allow you to run custom logic on a per-user basis to extend # the way directory-user attributes and groups are mapped to attributes, @@ -108,7 +110,7 @@ directory_users: # (required) connectors # The connectors section specifies how to connect User Sync to your directory. connectors: - # + # (optional) ldap (no default value) # ldap stand for "lightweight directory access protocol", which is the # network protocol used by most in-house directory systems (including @@ -120,7 +122,7 @@ directory_users: # location of this configuration file, not relative to the # working directory of your User Sync process. ldap: "connector-ldap.yml" - # + # (optional) csv (no default value) # csv stands for "comma-separated values", which is the most common form # of plain-text spreadsheet format. The value of the csv setting is @@ -130,26 +132,28 @@ directory_users: # use a relative pathname, it is interpreted relative to the # location of this configuration file, not relative to the # working directory of your User Sync process. - csv: "connector-csv.yml" + # [Uncomment the next line if you have a custom csv configuration file.] + #csv: "connector-csv.yml" # (optional) groups (no default value) - # The groups setting specifies how groups in the customer directory map + # The groups setting specifies how groups in the enterprise directory map # to product configurations and user groups on the Adobe side (collectively # called "Adobe groups"). This section is required only if you use the # --process-groups command-line argument. groups: - # the value of this setting is a mapping whose keys are single customer - # groups and whose values are lists of Adobe groups. This mapping is - # specified as a list of entries, each of which has a directory_group + # the value of this setting is a mapping whose keys are single enterprise + # directory groups and whose values are lists of Adobe groups. This mapping + # is specified as a list of entries, each of which has a directory_group # setting (whose value is a single directory group) and an adobe_groups # setting (whose value is a list of 0 or more product configuration and # user groups). All of the values in the adobe_groups settings must # match the name of product configurations and user groups which have # already been created on the Adobe side. (In this example, we pretend # that "Acrobat DC Pro" is a product configuration and "Copy Editors" - # is a user group that the customer has already created. Possibly + # is a user group that the you have already created. Possibly # the "Copy Editors" user group has been assigned access to appropriate # Adobe products, such as InDesign and InCopy.) + # [You will need to edit or remove these examples.] - directory_group: "Finance" adobe_groups: # the finance group doesn't use any Adobe products @@ -164,32 +168,32 @@ directory_users: # The limits section provides processing limits which can help ensure that # User Sync jobs do not exceed expected guardrails in their operation limits: - # + # (required) max_adobe_only_users # After initial population of users has been done on the Adobe side, # most User Sync jobs are expected to incrementally update and/or remove # just a few users at a time; that is, there aren't expected to be a lot - # of users on the Adobe side that aren't matched by users on the customer + # of users on the Adobe side that aren't matched by users on the directory # side. If there are a lot of these "Adobe-only" users found, it may indicate # a misconfiguration of the User Sync job, or possibly a temporary problem - # fetching users from the customer directory. In order to prevent a User Sync + # fetching users from the enterprise directory. In order to prevent a User Sync # job from doing something in these situations that is hard to repair, such as # removing or deleting a large number of users or entitlements, you can specify # the maximum number of Adobe-only users your User Sync job is expected to find. # In any run where the Adobe-only user count exceeds this limit, no updates # to the Adobe-only users are performed, so the effect of the job is limited to - # updating and/or creating Adobe users who match users on the customer side. + # updating and/or creating Adobe users. max_adobe_only_users: 200 # The logging section specifies what console or log file output # should be produced during each run of User Sync. logging: - # + # (optional) log_to_file (default value "False") # Whether you want logging done to a file in addition to the standard output. # allowed values are "True" (log to both) or "False" (log only to standard output). log_to_file: False - # + # (optional) file_log_directory (default value "logs") # An absolute or relative path to the directory where log files should be placed. # A single file is created per day, named with format "YYYY-MM-DD.log", and all @@ -197,15 +201,15 @@ logging: # If a relative path is specified, it is interpreted relative to this configuration # file, not relative to the User Sync process working directory. file_log_directory: logs - # + # (optional) file_log_level (default value "info") # This determines the detail level of the information logged to files. # Value must be one of "debug", "info", "warning", "error", or "critical", # in order of decreasing detail (that is, debug produces the most information, # while critical produces the least information, suppressing even reports # of non-fatal errors in operations.) - # file_log_level: info + # (optional) console_log_level (default value "info") # This determines the detail level of the information logged to standard output. # See the description of file_log_level for details of the allowed values. diff --git a/examples/config files - basic/3 connector-ldap.yml b/examples/config files - basic/3 connector-ldap.yml index 29a04a05d..1229b7bc9 100644 --- a/examples/config files - basic/3 connector-ldap.yml +++ b/examples/config files - basic/3 connector-ldap.yml @@ -16,10 +16,7 @@ # connection settings (required) # You must specify all four of these settings. Consult with your # enterprise directory administrators to get suitable values. -# You may want to specify these connection settings in a separate file -# from the rest of your settings, so as to guard your credential more -# securely than your other configuration values. See the User Sync -# documentation for an explanation of how to do this. +# These access credentials are sensitive and must be protected. username: "LDAP username goes here" password: "LDAP password goes here" host: "LDAP host URL goes here. e.g. ldap://ldap.example.com" @@ -76,7 +73,7 @@ group_filter_format: "(&(|(objectCategory=group)(objectClass=groupOfNames)(objec # (optional) user_email_format (default value given below) # user_email_format specifies how to construct a user's email address by # combining constant strings with the values of specific directory attributes. -# Any names in curly braces are take as attribute names, and everything including +# Any names in curly braces are taken as attribute names, and everything including # the braces will be replaced on a per-user basis with the values of the attributes. # The default value used here is simple, and suitable for OpenLDAP systems. If you # are using a non-email-aware AD system, which holds the username separately @@ -91,8 +88,8 @@ user_email_format: "{mail}" # (optional) user_username_format (no default value) # user_username_format specifies how to construct a user's username on the -# Adobe side by combining contstant strings with attribute values. -# Any names in curly braces are take as attribute names, and everything including +# Adobe side by combining constant strings with attribute values. +# Any names in curly braces are taken as attribute names, and everything including # the braces will be replaced on a per-user basis with the values of the attributes. # This setting should only be used when you are using federatedID and your # federation configuration specifies username-based login. In all other cases, diff --git a/examples/config files - basic/4 connector-csv.yml b/examples/config files - basic/4 connector-csv.yml index 4789bcf15..21f6f6e23 100644 --- a/examples/config files - basic/4 connector-csv.yml +++ b/examples/config files - basic/4 connector-csv.yml @@ -2,7 +2,7 @@ # # CSV (Comma-Separated Values) is a plain-text spreadsheet format. # The main function of this configuration file is to specify how column names -# in the spreadsheet are mapped to attribute names in the customer directory. +# in the spreadsheet are mapped to attribute names in the directory. # # The first line of a CSV file is assumed to be header row with column names. # The column names in your spreadsheet can be in any order, and if there are @@ -25,7 +25,7 @@ # Values in this column must be valid, unquoted email addresses. # A value is required in this column for all users, regardless # of their identity type. For Adobe ID users, all of the other -# column values are ignored. For Enterprise and Federated ID +# column values are optional. For Enterprise and Federated ID # users, all of the other column values are significant. email_column_name: email diff --git a/user_sync/config.py b/user_sync/config.py index f563bc617..a9d290078 100644 --- a/user_sync/config.py +++ b/user_sync/config.py @@ -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'], } diff --git a/user_sync/connector/directory_csv.py b/user_sync/connector/directory_csv.py index 2a3e8b27d..811f13cf5 100644 --- a/user_sync/connector/directory_csv.py +++ b/user_sync/connector/directory_csv.py @@ -65,7 +65,7 @@ def __init__(self, caller_options): builder.set_string_value('domain_column_name', 'domain') builder.set_string_value('identity_type_column_name', 'type') builder.set_string_value('user_identity_type', None) - builder.set_string_value('logger_name', 'connector.' + CSVDirectoryConnector.name) + builder.set_string_value('logger_name', CSVDirectoryConnector.name) builder.require_string_value('file_path') options = builder.get_options() diff --git a/user_sync/connector/directory_ldap.py b/user_sync/connector/directory_ldap.py index 05180b89e..8f4ec308f 100644 --- a/user_sync/connector/directory_ldap.py +++ b/user_sync/connector/directory_ldap.py @@ -66,7 +66,7 @@ def __init__(self, caller_options): builder.set_string_value('user_domain_format', None) builder.set_string_value('user_identity_type', None) builder.set_int_value('search_page_size', 200) - builder.set_string_value('logger_name', 'connector.' + LDAPDirectoryConnector.name) + builder.set_string_value('logger_name', LDAPDirectoryConnector.name) host = builder.require_string_value('host') username = builder.require_string_value('username') builder.require_string_value('base_dn') diff --git a/user_sync/connector/umapi.py b/user_sync/connector/umapi.py index 8a81aa1de..057c09c90 100644 --- a/user_sync/connector/umapi.py +++ b/user_sync/connector/umapi.py @@ -26,9 +26,9 @@ import helper import user_sync.config -import user_sync.error import user_sync.helper import user_sync.identity_type +from user_sync.error import AssertionException from user_sync.version import __version__ as APP_VERSION try: @@ -84,16 +84,20 @@ def __init__(self, name, caller_options): "client_secret": enterprise_options['client_secret'], "private_key_file": private_key_file_path } - self.connection = connection = umapi_client.Connection( - org_id=org_id, - auth_dict=auth_dict, - ims_host=ims_host, - ims_endpoint_jwt=server_options['ims_endpoint_jwt'], - user_management_endpoint=um_endpoint, - test_mode=options['test_mode'], - user_agent="user-sync/" + APP_VERSION, - logger=self.logger, - ) + try: + self.connection = connection = umapi_client.Connection( + org_id=org_id, + auth_dict=auth_dict, + ims_host=ims_host, + ims_endpoint_jwt=server_options['ims_endpoint_jwt'], + user_management_endpoint=um_endpoint, + test_mode=options['test_mode'], + user_agent="user-sync/" + APP_VERSION, + logger=self.logger, + ) + except Exception as e: + raise AssertionException("UMAPI connection to org id '%s' failed: %s" % (org_id, e)) + logger.debug('API initialized on: %s', um_endpoint) self.action_manager = ActionManager(connection, org_id, logger) @@ -184,7 +188,7 @@ def add_user(self, attributes): email = self.email if self.email else self.username if not email: errorMessage = "ERROR: you must specify an email with an Adobe ID" - raise user_sync.error.AssertionException(errorMessage) + raise AssertionException(errorMessage) params = self.convert_user_attributes_to_params({'email': email}) else: params = self.convert_user_attributes_to_params(attributes) diff --git a/user_sync/helper.py b/user_sync/helper.py index d41d98581..771526eba 100644 --- a/user_sync/helper.py +++ b/user_sync/helper.py @@ -69,7 +69,7 @@ def iter_csv_rows(file_path, delimiter = None, recognized_column_names = None, l if (recognized_column_names != None): unrecognized_column_names = [column_name for column_name in reader.fieldnames if column_name not in recognized_column_names] if (len(unrecognized_column_names) > 0 and logger != None): - logger.warn("Unrecognized column names: %s", unrecognized_column_names) + logger.warn("In file '%s': unrecognized column names: %s", file_path, unrecognized_column_names) for row in reader: yield row diff --git a/user_sync/rules.py b/user_sync/rules.py index 07e221180..2fafa450c 100644 --- a/user_sync/rules.py +++ b/user_sync/rules.py @@ -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, } @@ -89,9 +90,12 @@ 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() @@ -99,12 +103,11 @@ def __init__(self, caller_options): 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 = { @@ -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: @@ -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 @@ -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']: @@ -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)' @@ -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): ''' @@ -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: @@ -679,6 +688,7 @@ def update_umapi_users_for_connector(self, umapi_info, umapi_connector): options = self.options update_user_info = options['update_user_info'] manage_groups = self.will_manage_groups() + exclude_strays = self.options['exclude_strays'] will_process_strays = self.will_process_strays # prepare the strays map if we are going to be processing them @@ -688,12 +698,6 @@ def update_umapi_users_for_connector(self, umapi_info, umapi_connector): # there are certain operations we only do in the primary umapi in_primary_org = umapi_info.get_name() == PRIMARY_UMAPI_NAME - # we only log certain users if they are relevant to our processing. - log_excluded_users = update_user_info or manage_groups - log_stray_users = will_process_strays - log_matching_users = update_user_info or manage_groups - - # Walk all the adobe users, getting their group data, matching them with directory users, # and adjusting their attribute and group data accordingly. for umapi_user in umapi_connector.iter_users(): @@ -712,7 +716,7 @@ def update_umapi_users_for_connector(self, umapi_info, umapi_connector): desired_groups = user_to_group_map.pop(user_key, None) or set() # check for excluded users - if self.is_umapi_user_excluded(in_primary_org, user_key, current_groups, log_excluded_users): + if self.is_umapi_user_excluded(in_primary_org, user_key, current_groups): continue directory_user = filtered_directory_user_by_user_key.get(user_key) @@ -720,16 +724,17 @@ def update_umapi_users_for_connector(self, umapi_info, umapi_connector): # There's no selected directory user matching this adobe user # so we mark this adobe user as a stray, and we mark him # for removal from any mapped groups. - if log_stray_users: + if exclude_strays: + self.logger.debug("Excluding Adobe-only user: %s", user_key) + elif will_process_strays: self.logger.debug("Found Adobe-only user: %s", user_key) - if will_process_strays: self.add_stray(umapi_info.get_name(), user_key, None if not manage_groups else current_groups & umapi_info.get_mapped_groups()) else: # There is a selected directory user who matches this adobe user, # so mark any changed umapi attributes, # and mark him for addition and removal of the appropriate mapped groups - if log_matching_users: + if update_user_info or manage_groups: self.logger.debug("Adobe user matched on customer side: %s", user_key) if update_user_info and in_primary_org: attribute_differences = self.get_user_attribute_difference(directory_user, umapi_user) @@ -745,24 +750,21 @@ def update_umapi_users_for_connector(self, umapi_info, umapi_connector): umapi_info.set_umapi_users_loaded() return user_to_group_map - def is_umapi_user_excluded(self, in_primary_org, user_key, current_groups, do_logging): + def is_umapi_user_excluded(self, in_primary_org, user_key, current_groups): if in_primary_org: # in the primary umapi, we actually check the exclusion conditions identity_type, username, domain = self.parse_user_key(user_key) if identity_type in self.exclude_identity_types: - if do_logging: - self.logger.debug("Excluding adobe user (due to type): %s", user_key) + self.logger.debug("Excluding adobe user (due to type): %s", user_key) self.excluded_user_count += 1 return True if len(current_groups & self.exclude_groups) > 0: - if do_logging: - self.logger.debug("Excluding adobe user (due to group): %s", user_key) + self.logger.debug("Excluding adobe user (due to group): %s", user_key) self.excluded_user_count += 1 return True for re in self.exclude_users: if re.match(username): - if do_logging: - self.logger.debug("Excluding adobe user (due to name): %s", user_key) + self.logger.debug("Excluding adobe user (due to name): %s", user_key) self.excluded_user_count += 1 return True self.included_user_keys.add(user_key) @@ -907,32 +909,32 @@ 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) @@ -940,26 +942,35 @@ 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) diff --git a/user_sync/version.py b/user_sync/version.py index c7ff2d23d..a6bf7be98 100644 --- a/user_sync/version.py +++ b/user_sync/version.py @@ -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'