How to Sync Users and Roles Across Multiple Servers in Kentico

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.

Kentico Port Site Map

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

Share This Post:

Twitter Pinterest Facebook Google+
Click here to read more Kentico posts
Start a Project with Us

About the author

A Grand Valley State University graduate, Albert is the guy who takes on new technology and learns it inside and out. At BizStream, he mostly does SharePoint and Kentico development, but you can put him on any project and it will be a success. Albert has four adorable kids, and is a shark when it comes to Foosball and anything XBox-related.

View other posts by Albert

Subscribe to Updates

Stay up to date on what BizStream is doing and keep in the loop on the latest with Kentico.