The Problem
One of our clients has a website that has a port where they manage their content that comes from the boats. However, many of their users and roles are the same across different boats that they run. When someone is added/removed from a role, we need to be able to reflect those changes on every site below the port site.
While we could utilize things like SSO, or third-party solutions, we had requirements that didn’t allow for said options for various different reasons, so I had to manually synchronize the users.
How did I do this? I’ll let you know in 5 steps.
Disclaimer: The code’s in Kentico 8, but most of it will still work in modern versions.
Step 1 - Scheduled Tasks to the Rescue
I made every task to run on each of the boat sites. I’m not going to get into how to create a scheduled task.
Kentico has a pretty easy-to-follow guide on that which you can find
here.
This just creates the logging stuff, and then we call the guts of the code for User Importer (seen below)
public class PortSyncUsersTask : ITask
{
public string Execute(TaskInfo ti)
{
//Initialize the logs and setup any overhead
UserImporter ui = new UserImporter();
//Run the Importer
return ui.ImportUsers();
}
}
public string ImportUsers()
{
//1. Get all of the Port Users and Boat Users
List<UserInfo> allPortUsers = PortAPI.GetPortUsers();
if (allPortUsers == null)
return "Couldn't get the users from the Port";
List<UserInfo> allBoatUsers = UserInfoProvider.GetUsers().ToList();
//2. Map the users and update them
_uiMapper = new Mapper(_isDebug, _logBuilder);
_uiMapper.UpsertAllUsers(allBoatUsers, allPortUsers);
//3. Get the Boat and Port UserRoles
List<UserRoleInfo> portUserRoles = PortAPI.GetPortUserRoles();
List<UserRoleInfo> boatUserRoles = UserRoleInfoProvider.GetUserRoles().ToList();
//4. Get All of our Roles so we can map the UserRoles to something
List<RoleInfo> allportRoles = PortAPI.GetPortRoles();
if (allportRoles == null)
return "Couldn't get the roles from the Port";
List<RoleInfo> allBoatRoles = RoleInfoProvider.GetRoles().ToList();
//5. Map and Sync all of the User Roles
_uiMapper.UpsertAllUserRoles(portUserRoles, boatUserRoles, allBoatRoles, allportRoles);
return "successfully imported all Users";
}
Step 2 - Build the Kentico User Map
Quick Explanation
You may want to consider synchronizing your system based on ID or Guid, I had my own business requirements that didn't allow for those options. If your business requirements don’t allow for that, then I’m really glad you visited.
After I get the users from the boat, and from the port, I make a map of the user fields that I can access via Linq. The Kentico User Map is just an object that contains a port user and a boat user. We want it to look something like this when we are done.
Kentico User Map Example
PortUserInfo |
BoatUserInfo |
Albert H
UserID: 6
Address: 123 Fake St. MI
Email:test@test.com
|
Albert H
UserID: 60
Address: 123 Fake St. MI
Email:test@test.com
|
Alex H
UserID: 5
Address: 1223 Fake St. MI
Email:justyouwait@gmail.com
|
Alex H
UserID: 61
Address: 4556 Newman St. NY
Email:justyouwait@gmail.com
|
UserName: Ryan W
UserID: 4
Address: 123 Farrison St. MI
Email:what@scsa.com |
|
|
UserName: Aaron B
UserID: 62
Address: 555 Test St. MI
Email: mynameis@burr.sir
|
As you can see in the code, first I add all of the boat users and every port user that matches a username on the boat. Then I get all of the port users that didn't match a username from our boat.
private void MapUsers(List<UserInfo> BoatUsers, List<UserInfo> PortUsers)
{
_mappedUsers = BoatUsers.Select(x => new UserMap
{
BoatUser = x,
PortUser = PortUsers.SingleOrDefault(y => y.UserName == x.UserName)
}).ToList();
List<UserMap> newPortUsers = PortUsers
.Where(pu => BoatUsers.All(bu => bu.UserName != pu.UserName))
.Select(x => new UserMap
{
PortUser = x
}).ToList();
_mappedUsers.AddAll<UserMap>(newPortUsers);
}
Step 3 - Synchronize Based on the Map
1.
Delete - This is pretty easy, just call UserInfoProvider.DeleteUser for the user that isn't on the port. I'm going to put a disclaimer here. Whenever you are automatically deleting anything... be very careful. If you aren't careful in your coding/testing you could blow away an entire user base. Don't do that, test it well.
private List<UserInfo> DeleteUsers()
{
List<UserInfo> removedUsers = _mappedUsers.Where(u => u.PortUser == null && u.BoatUser != null)
.Select(u =>
{
UserInfoProvider.DeleteUser(u.BoatUser.UserID);
return u.BoatUser;
}).ToList();
//Remove deleted users for when we update the User Roles
_mappedUsers = _mappedUsers.Where(u => u.PortUser != null).ToList();
return removedUsers;
}
2.
Add - just get the mapped Users from the port that aren't in the boat, then add them by using UserInfoProvider.SetUserInfo. Adding itself is a little tricky because you want to keep all of the fields the same without thinking about it.
private List<UserInfo> AddUsers()
{
return _mappedUsers.Where(userMap => userMap.PortUser != null && userMap.BoatUser == null)
.Select(userMap =>
{
UserInfo userToAddToBoat = UserInfo.New(userMap.PortUser);
userMap.BoatUser = AddUser(userToAddToBoat);
return userMap.BoatUser;
}).ToList();
}
private static UserInfo AddUser(UserInfo portUser)
{
// zero for a new user
portUser.UserID = 0;
UserInfoProvider.SetUserInfo(portUser);
UserInfoProvider.AddUserToSite(portUser.UserName, SiteContext.CurrentSiteName);
return UserInfoProvider.GetUserInfo(portUser.UserName);
}
Make sure that if you add this way, that you create a new instance of the UserInfo, or your Map will update your portUserID to be what you passed in. If not, then you'll have a ton of problems when mapping the user roles.
3.
Update - The update function you see below updates the properties that I wanted to be updated by calling UserInfo.SetValue then updates the user. You can set whatever properties here that you want to be synced.
private List<UserInfo> UpdateUsers()
{
return _mappedUsers.Where(userMap =>
userMap.PortUser != null && userMap.BoatUser != null &&
userMap.PortUser.UserLastModified > userMap.BoatUser.UserLastModified)
.Select(userMap =>
{
UpdateUser(userMap.BoatUser, userMap.PortUser);
return userMap.BoatUser;
}).ToList();
}
private static void UpdateUser(UserInfo boatUser, UserInfo portUser)
{
boatUser.SetValue("UserEnabled", portUser.UserEnabled);
boatUser.SetValue("Address", portUser.FirstName);
boatUser.SetValue("Email", portUser.Email);
boatUser.Update();
}
While I'm leaving the role mapping part out, you can use the same principals that we used to map the users. Once you have the users/roles mapped you can move to the next part of this blog post,
Synchronizing the User-Roles to Other Kentico Sites.