Pages

Monday, April 6, 2009

How To Code and Synchronize SharePoint WSS and MOSS User Profiles


I created the UserProfileMgr.cs class to manage and synchronize the WSS and MOSS user profiles for a FBA project. It is different from the other article I wrote How To Add Users to Groups using an HttpModule for a Forms Authentication (FBA) SharePoint Site in that this doesn't use the web services to update the MOSS user profile and it has key lines of code that overcome security issues that are necessary when working with SharePoint WSS and MOSS user profiles. All service packs and hot fixes are installed up to April 1 2009.
I used similar code from my earlier article that implemented RunWithElevatedPrivileges and AllowUnsafeUpdates. The code worked fine on a computer that was not connected to a domain but I found the UpdateMOSSUserProfile() code would get security validation errors when I logged in using a domain user account running on a Virtual PC using Windows Server 2003 with a dynamic IP connected through VPN. After several weeks of frustration I discovered that 3 additional lines of code made everything work.
To illustrate these validation failures I have removed those 3 lines of code from the UserProfileMgr.cs file and then added them back one at a time to observe the variety of errors created when you run the application. You will discover the failures all come from the UpdateMOSSUserProfile() routine.


Here are the steps I followed and the errors I found. To reproduce this you will need to login as both an existing and as a new user using a Forms Authenticated page.

1. Take out just 3 lines of code in UserProfileMgr.cs at lines 50, 55 and 175.
    50 currentWeb.AllowUnsafeUpdates = true;
    55 spWeb.Update();
    175 HttpContext.Current = null;

The new user fails at line 197
    userProfileManager.CreateUserProfile(userLoginName)

with the error
    "Unable to evaluate expression because the code is optimized or a native frame is on top of the call stack."

The existing user fails at line 274
    userProfile.Commit();

with the error
    "The security validation for this page is invalid. Click Back in your Web browser, refresh the page, and try your operation again."

2. Add only the line 55 back in with the other 2 lines removed.
    55 spWeb.Update();

The new user fails at line 197
    userProfileManager.CreateUserProfile(userLoginName)

with the error
    "Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))"
    or
    "Unable to evaluate expression because the code is optimized or a native frame is on top of the call stack."

The existing user fails at line 274
    userProfile.Commit();

with the error
    "Updates are currently disallowed on GET requests. To allow updates on a GET, set the 'AllowUnsafeUpdates' property on SPWeb."

3. Add only the line 50 back in with the other 2 lines removed.
    50 currentWeb.AllowUnsafeUpdates = true;

The new user works at line 197
    userProfileManager.CreateUserProfile(userLoginName)

but fails at line 274
    userProfile.Commit();

with the error
    "Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))"

The existing user now works correctly at line 274.
    userProfile.Commit();

4. Add lines 50 and 55 back in with the other line removed.
    50 currentWeb.AllowUnsafeUpdates = true;
    55 spWeb.Update();

The new user fails at line 274
    userProfile.Commit();

with the error
    "Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))"
    or
    "Unable to evaluate expression because the code is optimized or a native frame is on top of the call stack."

5. Add all 3 line back in.
    50 currentWeb.AllowUnsafeUpdates = true;
    55 spWeb.Update();
    175 HttpContext.Current = null;

The new user now works correctly at line 274
    userProfile.Commit();

//---------------------------------------------------------------------
//UserProfileMgr.cs:
//---------------------------------------------------------------------
Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using System.Web;
using System.Web.Security;
using System.Data.SqlClient;
using System.Data;
using Microsoft.Office.Server;
using Microsoft.Office.Server.UserProfiles;

namespace OPCMOSS
{
    /// <summary>
    /// Class for User WSS and MOSS Profile handling
    /// </summary>
    public class UserProfileMgr
    {

        /// <summary>
        /// Update WSS and MOSS User Profiles
        /// </summary>
        /// <param name="spSite"></param>
        /// <param name="spWeb"></param>
        /// <param name="userLoginName"></param>
        /// <param name="userName"></param>
        /// <param name="displayName"></param>
        /// <param name="firstName"></param>
        /// <param name="lastName"></param>
        /// <param name="countryName"></param>
        /// <returns></returns>
        public SPUser UpdateUserProfile(SPSite spSite, SPWeb spWeb, string userLoginName, string userName, string displayName,
            string firstName, string lastName, string countryName)
        {
            SPUser spUser;
          
            currentWeb = SPContext.Current.Web;
            Guid webId = currentWeb.ID;
            Guid siteId = currentWeb.Site.ID;

            //(Note: under a forms authenticated site, RunWithElevatedPriviliges uses the application pool account)
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite spSite = new SPSite(siteId))
                {
                    using (SPWeb spWeb = spSite.OpenWeb(webId))
                    {
                        bool SavedCurrentWebAllowUnsafeUpdates = currentWeb.AllowUnsafeUpdates;
                        currentWeb.AllowUnsafeUpdates = true;
                        bool SavedspSiteAllowUnsafeUpdates = spSite.AllowUnsafeUpdates;
                        spSite.AllowUnsafeUpdates = true;
                        bool SavedspWebAllowUnsafeUpdates = spWeb.AllowUnsafeUpdates;
                        spWeb.AllowUnsafeUpdates = true;
                        spWeb.Update();

                        try
                        {
                            //Update user display name
                            //and WSS User Profile ("User Information List").
                            spUser = UpdateWSSUserProfile(spWeb, userLoginName, userName,
                                displayName, firstName, lastName, countryName);

                            //Add or update MOSS User Profile.
                            UpdateMOSSUserProfile(spSite, userLoginName, userName,
                                displayName, firstName, lastName, countryName);

                            //Add or update user to the assigned Group.
                            AddUserToAssignedGroup(spWeb, spUser);

                        }
                        catch (Exception ex)
                        {
                            throw new SPException("Error: SynchData: Unable to set the User Profile.  " + ex.Message);
                        }
                        finally
                        {
                            spWeb.AllowUnsafeUpdates = SavedspWebAllowUnsafeUpdates;
                            spSite.AllowUnsafeUpdates = SavedspSiteAllowUnsafeUpdates;
                            currentWeb.AllowUnsafeUpdates = SavedCurrentWebAllowUnsafeUpdates;
                            if (spWeb != null) spWeb.Dispose();
                            if (spSite != null) spSite.Dispose();
                        }
                    }
                }
            });

            return spUser;
        }

        /// <summary>
        /// Update WSS User Profile ("User Information List" site collection)
        /// </summary>
        /// <param name="spWeb"></param>
        /// <param name="userLoginName"></param>
        /// <param name="userName"></param>
        /// <param name="displayName"></param>
        /// <param name="firstName"></param>
        /// <param name="lastName"></param>
        /// <param name="countryName"></param>
        /// <returns></returns>
        public SPUser UpdateWSSUserProfile(SPWeb spWeb, string userLoginName, string userName, string displayName,
            string firstName, string lastName, string countryName)
        {
            // Get the current user from AllUsers "User Information List" site collection
            //SPUser spUser = spWeb.AllUsers[userLoginName];
            SPUser spUser = spWeb.EnsureUser(userLoginName);

            // Update the display Name and work Email properties in the "User Information List" site collection
            spUser.Name = displayName;
            //spUser.Email = WorkEmail;
            spUser.Update();

            // The other user information is in SPListItem
            // spWeb.SiteUserInfoList.GetItemById(some_id) is about 5 time faster
            // than spWeb.SiteUserInfoList.Items[some_id]
            SPList siteUserInfoList = spWeb.SiteUserInfoList;

            // Add Country field to user list if missing
            if (!siteUserInfoList.Fields.ContainsField("Country"))
            {
                siteUserInfoList.Fields.Add("Country", SPFieldType.Text, false);
                siteUserInfoList.Update();
            }

            SPListItem userItem = siteUserInfoList.GetItemById(spUser.ID);

            // "User Information List" site collection built-in items
            // Title, Name, EMail, Notes, SipAddress, Locale, IsSiteAdmin, Picture, Department, JobTitle, IsActive,
            // FirstName, LastName, WorkPhone, UserName, WebSite, Office, ID, Modified, Created, Author, Editor

            // Note: Following values are set by UserGroup.AddUserToGroup():
            // userItem["Name"] is the Account login name
            // userItem["Title"] is the display name and the user.Name
            // userItem["EMail"] is the Work Email and the user.Email
            // userItem["Notes"] is the userNotes
            // userItem["Modified"] is handled by SharePoint
            // Note: if user is deleted and re-created, userItem["Created"] field remains the date first created.
            // userItem["Created"] is handled by SharePoint

            // UserName is the UserKey (frmOPCUser:740982).
            // This matches with what CreateUserProfile() will create.
            userItem["UserName"] = userName;

            userItem["FirstName"] = firstName;
            userItem["LastName"] = lastName;
            userItem["Country"] = countryName;
            userItem["Modified"] = DateTime.Now;
            userItem.Update();

            return spUser;
        }

        /// <summary>
        /// Add or Update MOSS User Profile database
        /// </summary>
        /// <param name="spSite"></param>
        /// <param name="userLoginName"></param>
        /// <param name="userName"></param>
        /// <param name="displayName"></param>
        /// <param name="firstName"></param>
        /// <param name="lastName"></param>
        /// <param name="countryName"></param>
        public void UpdateMOSSUserProfile(SPSite spSite, string userLoginName, string userName, string displayName,
            string firstName, string lastName, string countryName)
        {
            bool newUser = false;

            //Bug workaround: The MOSS UserProfileManager class has a bad intermittant bug
            // causing the userProfile.Commit() to get an access denied error even though
            // the code uses RunWithElevatedPrivileges().
            // Refer to "SharePoint User Profile Access Issues".
            // http://blog.mikehacker.net/2008/08/21/sharepoint-user-profile-access-issues/
            HttpContext myContext = HttpContext.Current;
            HttpContext.Current = null;

            try
            {
                //ServerContext object of current site
                ServerContext context = ServerContext.GetContext(spSite);

                //UserProfileManager object to access MOSS user profiles
                UserProfileManager userProfileManager = new UserProfileManager(context);

                //Check whether user of given account name exists or not
                if (!userProfileManager.UserExists(userLoginName))
                {
                    // Add User to the MOSS User Profile database.
                    // Following properties are updated:
                    // AccountName = frmUser:740984
                    // UserName = 740984

                    // Note: After the user is added to the MOSS User Profile database using
                    // CreateUserProfile() the AccountName and Username properties are eventually synchronized
                    // between the MOSS User Profile database and the WSS "User Information List":

                    userProfileManager.CreateUserProfile(userLoginName);
                    newUser = true;
                }

                //Find the user profile of given account name
                UserProfile userProfile = userProfileManager.GetUserProfile(userLoginName);

                //Available built-in properties
                //AboutMe, AccountName, ADGuid, Assistant, Birthday, CellPhone, Department, DirectReports,
                //DontSuggestList, Dottedline, Fax, FirstName, HireDate, HomePhone, Interests,
                //LastColleagueAdded, LastName, Manager, MasterAccountName, MySiteUpgrade, Office,
                //OutlookWebAccessUrl, PastProjects, Peers, PersonalSpace, PictureUrl, PreferredName,
                //PublicSiteRedirect, QuickLinks, ResourceAccountName, ResourceSID, Responsibility,
                //School, SID, SipAddress, Skills, Title, UserGuid, UserName, WebSite, WorkEmail, WorkPhone

                //Assigning user profile values
                userProfile["PreferredName"].Value = displayName;
                userProfile["FirstName"].Value = firstName;
                userProfile["LastName"].Value = lastName;

                // If property missing.
                const string CountryProperty = "Country";
                if (userProfileManager.Properties.GetPropertyByName(CountryProperty) == null)
                {
                    // Add Country custom field to user profile if missing
                    Microsoft.Office.Server.UserProfiles.PropertyCollection pc = userProfileManager.Properties;
                    Property p = pc.Create(false);
                    p.Name = CountryProperty;
                    p.DisplayName = CountryProperty;
                    p.Type = PropertyDataType.String;
                    p.Length = 255;
                    p.IsUserEditable = true;
                    p.PrivacyPolicy = PrivacyPolicy.OptIn;
                    p.DefaultPrivacy = Privacy.Public;
                    pc.Add(p);
                }
                userProfile["Country"].Value = countryName;

                // If property missing.
                const string ModifiedProperty = "Modified";
                if (userProfileManager.Properties.GetPropertyByName(ModifiedProperty) == null)
                {
                    // Add Modified custom field to user profile if missing
                    Microsoft.Office.Server.UserProfiles.PropertyCollection pc = userProfileManager.Properties;
                    Property p = pc.Create(false);
                    p.Name = ModifiedProperty;
                    p.DisplayName = ModifiedProperty;
                    p.Type = PropertyDataType.DateTime;
                    p.IsUserEditable = true;
                    p.PrivacyPolicy = PrivacyPolicy.OptIn;
                    p.DefaultPrivacy = Privacy.Public;
                    pc.Add(p);
                }
                userProfile["Modified"].Value = DateTime.Now;

                //If new user
                if (newUser)
                {
                    // If property missing.
                    const string CreatedProperty = "Created";
                    if (userProfileManager.Properties.GetPropertyByName(CreatedProperty) == null)
                    {
                        // Add Created custom field to user profile if missing
                        Microsoft.Office.Server.UserProfiles.PropertyCollection pc = userProfileManager.Properties;
                        Property p = pc.Create(false);
                        p.Name = CreatedProperty;
                        p.DisplayName = CreatedProperty;
                        p.Type = PropertyDataType.DateTime;
                        p.IsUserEditable = false;
                        p.PrivacyPolicy = PrivacyPolicy.OptIn;
                        p.DefaultPrivacy = Privacy.Public;
                        pc.Add(p);
                    }
                    userProfile["Created"].Value = DateTime.Now;
                }

                // Update the user profile.
                userProfile.Commit();
            }
            catch (Exception ex)
            {
                throw new SPException("Error: UpdateMOSSUserProfile: Unable to set the User Profile.  " + ex.Message);
            }
            finally
            {
                HttpContext.Current = myContext;
            }
        }

        /// <summary>
        /// Adds a user to their assigned Group
        /// </summary>
        /// <param name="spWeb"></param>
        /// <param name="spUser"></param>
        public void AddUserToAssignedGroup(SPWeb spWeb, SPUser spUser)
        {
            string groupName = "Member Group";
            EnsureSiteGroup(spWeb, groupName);


            //If group found
            if (groupName != null)
            {
                try
                {
                    //Add user to the Group
                    spWeb.SiteGroups[groupName].AddUser(spUser);
                }
                catch (Exception ex)
                {
                    throw new Exception("The site does not have the sharepoint Group configured.);
                }
            }
            else
            {
                throw new SPException("The user is not assigned to an Group in the database.");
            }
        }

        /// <summary>
        /// Ensure group exists and if not add it.
        /// </summary>
        /// <param name="spWeb"></param>
        /// <param name="groupName"></param>
        public void EnsureSiteGroup(SPWeb spWeb, string groupName)
        {
            SPGroup spGroup = null;
            string groupAssociations = string.Empty;
            SPGroup spGroupOwner = spWeb.AssociatedOwnerGroup;

            spGroup = GetSiteGroup(spWeb, groupName);
            if (spGroup == null)
            {
                spWeb.SiteGroups.Add(groupName, spGroupOwner, null, groupName);
                spGroup = GetSiteGroup(spWeb, groupName);
                if (spGroup != null)
                {
                    SPRoleAssignment roleAssignment = new SPRoleAssignment(spGroup);
                    SPRoleDefinition roleDefinition = spWeb.RoleDefinitions.GetByType(SPRoleType.Reader);
                    roleAssignment.RoleDefinitionBindings.Add(roleDefinition);
                    spWeb.RoleAssignments.Add(roleAssignment);
                }
            }
        }

        /// <summary>
        /// Searches for a requested group and returns SSPGroup object
        /// </summary>
        /// <param name="spWeb"></param>
        /// <param name="groupName"></param>
        /// <returns>SSPGroup</returns>
        public SPGroup GetSiteGroup(SPWeb spWeb, string groupName)
        {
            foreach (SPGroup group in spWeb.SiteGroups)
                if (group.Name.ToLower() == groupName.ToLower())
                    return group;
            return null;
        }

    }
}

No comments: