Tuesday, 23 September 2014

Synchronizing user image between Active Directory and SharePoint profile

As you may know Exchange 2010 and Outlook 2010 use the Picture attribute (better known as thumbnailPhoto based on its LDAP name) to store user and contact photos in Active Directory. You can learn more about that from this post in the Microsoft Exchange Team Blog.
Since I found no way in Outlook 2010 to set the photo, and found only (for an end user) cumbersome admin tools, like PowerShell and VBScript to upload the image. On the other side, SharePoint makes it possible for a user to upload her / his photo to the profile from the web UI easily.
Before trying to reinvent the wheel, I looked around on the web for similar solutions, but found only implementations (like this one) where an extension attribute of AD (like extensionAttribute3) was mapped to the ProfileURL SharePoint profile property.
My goal was a bit more: to synchronize the image binary as well, not only URLs. As I’ve already created tools to upload user photos from file system to SharePoint profile programmatically I thought it would be nice to reuse that knowledge on this task.
Although an ideal solution might be some kind of server process (for example a timer job), for the sake of simplicity and visibility I chose a Visual Web Part-based “self-service” implementation.
Let’s see the results first. You can find the code and the binaries as a single file download here.
When deploy the solution, you have to add the Profile Photo Sync WebPart to a page…
image
…and set its Root application partition property to the root of your AD.
image
Assuming there is no user photo in the profile nor in the AD, you should see something similar first:
image
Let’s upload our photo quickly to our SharePoint profile. It is done, the image is shown on the profile page. Let’s back to the web part. But wait a minute! The photo is still not visible there! What happened? I told you: wait a minute. SharePoint requires about one minute to push the profile property changes to the site. After it is done, the image is visible in the web part as expected:
image
If you press the >> button, the image is transferred to AD.
image
Now delete the image from the profile and refresh the page. The image can be found in the AD, but not in the SharePoint profile.
image
Try to load it back to SharePoint by pressing the << button. It seems to have no effect, but again you should wait a minute to see the result:
image
If you delete the image from the SharePoint profile again, refresh the page and now click on the >> button, you find that the photo is deleted from the AD as well and we are back to our starting point. Synchronizing an empty thumbnailPhoto property from AD to SharePoint profile deletes the image from the profile as well.
After this short demo let’s see some code blocks.
Since our control has to know where to look for AD properties we have to provide this iannformation for it. The web part has a property called DefaultPartition. The value of this property is forwarded to the user control in the CreateChildControls method.
  1. [Personalizable(),
  2. WebBrowsable(),
  3. WebDisplayName("Root application partition")]
  4. public string DefaultPartition
  5. {
  6.     get;
  7.     set;
  8. }
  9. #endregion
  10.  
  11. protected override void CreateChildControls()
  12. {
  13.     ProfilePhotoSyncWebPartUserControl control = (ProfilePhotoSyncWebPartUserControl)Page.LoadControl(_ascxPath);
  14.     control.DefaultPartition = DefaultPartition;
  15.     Controls.Add(control);
  16. }
The RefreshControls method is responsible for rendering the content of the UI. Since there is no built-in way to refer the thumbnailPhoto property in the AD via a URL, I made a quick and dirty hack there. If the request URL contains the UserName query string parameter, we clears the response, get the binary content from the thumbnailPhoto property and push that content to the response.
If the UserName query string parameter is not found in the request URL and the thumbnailPhoto property is not empty, I set the image URL of the AD image to the URL of the current page appending the current user name in the UserName query string parameter.
Similarly, if the PictureUrl user profile property is not empty in SharePoint I set the image URL for the SharePoint image to the value stored in the PictureUrl property.
If one or both of the properties would be empty then the corresponding image is hidden and a No photo label is shown as you can see on the images above.
  1. private void RefreshControls()
  2.         {
  3.             String userName = Request.QueryString["UserName"];
  4.  
  5.             // hack: render response as image for AD "thumbnailPhoto" image
  6.             if (!String.IsNullOrEmpty(userName))
  7.             {
  8.                 Response.Clear();
  9.                 ADUtils adUtils = new ADUtils(DefaultPartition);
  10.                 using (DirectoryEntry root = adUtils.GetDefaultPartition())
  11.                 using (DirectoryEntry user = adUtils.FindUserByAccountName(userName))
  12.                 {
  13.                     byte[] thumbnailPhotoBytes = (byte[])user.Properties["thumbnailPhoto"].Value;
  14.                     if (thumbnailPhotoBytes != null)
  15.                     {
  16.                         Response.BinaryWrite(thumbnailPhotoBytes);
  17.                     }
  18.  
  19.                 }
  20.                 Response.End();
  21.             }
  22.             else
  23.             {
  24.                 try
  25.                 {
  26.                     ADUtils adUtils = new ADUtils(DefaultPartition);
  27.                     using (DirectoryEntry root = adUtils.GetDefaultPartition())
  28.                     using (DirectoryEntry user = adUtils.FindUserByAccountName(_shortName))
  29.                     {
  30.                         byte[] thumbnailPhotoBytes = (byte[])user.Properties["thumbnailPhoto"].Value;
  31.                         bool hasThumbnailPhoto = (thumbnailPhotoBytes != null);
  32.                         ADImage.Visible = hasThumbnailPhoto;
  33.                         ADImageLabel.Visible = !hasThumbnailPhoto;
  34.  
  35.                         if (hasThumbnailPhoto)
  36.                         {
  37.                             ADImage.ImageUrl = String.Format("{0}?UserName={1}", Request.Url.GetLeftPart(UriPartial.Path), _shortName);
  38.                         }
  39.  
  40.                     }
  41.                 }
  42.                 catch (Exception ex)
  43.                 {
  44.                     Warning.Visible = true;
  45.                 }
  46.  
  47.                 UserProfileManager userProfileManager = new UserProfileManager(SPServiceContext.Current);
  48.                 UserProfile userProfile = userProfileManager.GetUserProfile(_accountName);
  49.  
  50.                 String pictureUrl = (String)userProfile["PictureUrl"].Value;
  51.  
  52.                 bool hasProfilelPhoto = (!String.IsNullOrEmpty(pictureUrl));
  53.  
  54.                 // double check to be sure the file is there to avoid "missing" images on the page
  55.                 // when removing the profile image on the UI it takes some time the change get reflected
  56.                 // in the profile property, but image is deleted immediately
  57.                 if (hasProfilelPhoto)
  58.                 {
  59.                     using (SPSite site = new SPSite(pictureUrl))
  60.                     {
  61.                         using (SPWeb web = site.OpenWeb())
  62.                         {
  63.                             SPFile file = web.GetFile(pictureUrl);
  64.                             hasProfilelPhoto = file.Exists;
  65.                         }
  66.                     }
  67.                 }
  68.  
  69.                 SPImage.Visible = hasProfilelPhoto;
  70.                 SPImageLabel.Visible = !hasProfilelPhoto;
  71.  
  72.                 if (hasProfilelPhoto != null)
  73.                 {
  74.                     SPImage.ImageUrl = pictureUrl;
  75.                 }
  76.  
  77.             }
  78.         }
When you click on the >> button to copy SharePoint profile image to AD the following code is called:
  1. protected void SP2AD_Click(object sender, EventArgs e)
  2.         {
  3.  
  4.             UserProfileManager userProfileManager = new UserProfileManager(SPServiceContext.Current);
  5.             UserProfile userProfile = userProfileManager.GetUserProfile(_accountName);
  6.  
  7.             String pictureUrl = (String)userProfile["PictureUrl"].Value;
  8.  
  9.             bool hasProfilelPhoto = (!String.IsNullOrEmpty(pictureUrl));
  10.  
  11.             byte[] imageContent = null;
  12.  
  13.             // double check to be sure the file is there to avoid "missing" images on the page
  14.             // when removing the profile image on the UI it takes some time the change get reflected
  15.             // in the profile property, but image is deleted immediately
  16.             if (hasProfilelPhoto)
  17.             {
  18.                 using (SPSite site = new SPSite(pictureUrl))
  19.                 {
  20.                     using (SPWeb web = site.OpenWeb())
  21.                     {
  22.                         SPFile file = web.GetFile(pictureUrl);
  23.                         if (file.Exists)
  24.                         {
  25.                             imageContent = file.OpenBinary();
  26.                         }
  27.                     }
  28.                 }
  29.             }
  30.  
  31.             ADUtils adUtils = new ADUtils(DefaultPartition);
  32.             using (DirectoryEntry root = adUtils.GetDefaultPartition())
  33.             using (DirectoryEntry user = adUtils.FindUserByAccountName(_shortName))
  34.             {
  35.                 user.Properties["thumbnailPhoto"].Clear();
  36.                 if (imageContent != null)
  37.                 {
  38.                     user.Properties["thumbnailPhoto"].Add(imageContent);
  39.                 }
  40.                 user.CommitChanges();
  41.  
  42.             }
  43.  
  44.             RefreshControls();
  45.  
  46.         }
Similarly, when you click << to copy image from AD to the SharePoint profile, the following code gets executed:
  1. protected void AD2SP_Click(object sender, EventArgs e)
  2.       {
  3.           try
  4.           {
  5.               ADUtils adUtils = new ADUtils(DefaultPartition);
  6.               using (DirectoryEntry root = adUtils.GetDefaultPartition())
  7.               using (DirectoryEntry user = adUtils.FindUserByAccountName(_shortName))
  8.               {
  9.                   byte[] thumbnailPhotoBytes = (byte[])user.Properties["thumbnailPhoto"].Value;
  10.                   bool hasThumbnailPhoto = (thumbnailPhotoBytes != null);
  11.                   ADImage.Visible = hasThumbnailPhoto;
  12.                   ADImageLabel.Visible = !hasThumbnailPhoto;
  13.  
  14.                   if (hasThumbnailPhoto)
  15.                   {
  16.                       using (SPSite site = new SPSite(GetMySiteHostUrl(SPContext.Current.Site)))
  17.                       {
  18.                           site.AllowUnsafeUpdates = true;
  19.                           using (SPWeb web = site.OpenWeb())
  20.                           {
  21.                               web.AllowUnsafeUpdates = true;
  22.                               ProfileImagePicker profileImagePicker = new ProfileImagePicker();
  23.                               InitializeProfileImagePicker(profileImagePicker, web);
  24.                               SPFolder subfolderForPictures = GetSubfolderForPictures(profileImagePicker);
  25.  
  26.                               UploadPhoto(_accountName, thumbnailPhotoBytes, subfolderForPictures);
  27.                               SetPictureUrl(_accountName, subfolderForPictures);
  28.                           }
  29.                       }
  30.                   }
  31.               }
  32.           }
  33.           catch (Exception ex)
  34.           {
  35.               Warning.Visible = true;
  36.           }
  37.  
  38.           RefreshControls();
  39.  
  40.       }
Helper methods in the AD2SP_Click method (UploadPhoto, SetPictureUrl, GetMySiteHostUrl etc.) are similar to the ones used in my former solutions here and here.
Active Directory related code is included in the ADUtils class (see credits later):
  1. public class ADUtils
  2.     {
  3.  
  4.         public ADUtils(String defaultPartition)
  5.         {
  6.             DefaultPartition = defaultPartition;
  7.         }
  8.  
  9.         private String DefaultPartition
  10.         {
  11.             get;
  12.             set;
  13.         }
  14.  
  15.         /// <summary>
  16.         /// Retrieves a DirectoryEntry using configuration data
  17.         /// </summary>
  18.         /// <param name="path"></param>
  19.         /// <returns></returns>
  20.         public DirectoryEntry CreateDirectoryEntry(string path)
  21.         {
  22.             return new DirectoryEntry(String.Format("LDAP://{0}", path));
  23.         }
  24.  
  25.         /// <summary>
  26.         /// Creates a DirectoryEntry from the DefaultPartition defined in config
  27.         /// </summary>
  28.         /// <returns></returns>
  29.         public DirectoryEntry GetDefaultPartition()
  30.         {
  31.             return CreateDirectoryEntry(DefaultPartition);
  32.         }
  33.  
  34.         /// <summary>
  35.         /// Simple method to find and return a user using the CN name and searching the
  36.         /// defaultNamingContext defined in config.
  37.         /// </summary>
  38.         /// <param name="userRDN"></param>
  39.         /// <returns></returns>
  40.         public DirectoryEntry FindUserByCN(string userRDN)
  41.         {
  42.             using (DirectoryEntry searchRoot = GetDefaultPartition())
  43.             {
  44.                 DirectorySearcher ds = new DirectorySearcher(
  45.                     searchRoot,
  46.                     String.Format("(cn={0})", userRDN),
  47.                     new string[] { "cn" },
  48.                     SearchScope.Subtree
  49.                     );
  50.  
  51.                 SearchResult sr = ds.FindOne();
  52.  
  53.                 return (sr != null) ? sr.GetDirectoryEntry() : null;
  54.             }
  55.         }
  56.  
  57.         /// <summary>
  58.         /// Simple method to find and return a user using the sAMAccountName name and searching the
  59.         /// defaultNamingContext defined in config.
  60.         /// </summary>
  61.         /// <param name="userRDN"></param>
  62.         /// <returns></returns>
  63.         public  DirectoryEntry FindUserByAccountName(string accountName)
  64.         {
  65.             Trace.TraceInformation("FindUserByAccountName method called. Account name: '{0}'", accountName);
  66.             using (DirectoryEntry searchRoot = GetDefaultPartition())
  67.             {
  68.                 DirectorySearcher ds = new DirectorySearcher(
  69.                     searchRoot,
  70.                     String.Format("(sAMAccountName={0})", accountName),
  71.                     new string[] { "sAMAccountName" },
  72.                     SearchScope.Subtree
  73.                     );
  74.  
  75.                 SearchResult sr = ds.FindOne();
  76.                 Trace.TraceInformation("FindUserByAccountName user found. Path: '{0}'", sr.Path);
  77.                 return (sr != null) ? sr.GetDirectoryEntry() : null;
  78.             }
  79.         }
  80.  
  81.     }
Note: The sample web part provided “as is”, and is intended to be used only as a proof of concept.
Important point on security: The sample should work only when at least two of the followings are located on the same computer: browser, SharePoint 2010 front end, Active Directory domain controller. If all of these three are located on a different computer and there is no Kerberos implemented then you should be prepared for the so-called double hops issue.
My general recommendation for that problem is accessing the resource (in this case the AD) using a service user account that has access to all the requested information. One can store the service account credentials for example in SSO or in an encrypted configuration parameter.
Implementing this features require some modifications in the current code. For example, you have to use a DirectoryEntry constructor in the CreateDirectoryEntry method of ADUtils class that has user name and password parameters.
You will not have such issues if you alter the solution to a server process, like timer job that runs as a dedicated user account.
Credits: I would like to say thanks to Joe Kaplan and Ryan Dunn for their book The .NET Developer’s Guide to Directory Services Programming. The book and the code samples are definitely a must for every .NET programmer who wants (or has) to dive into the beauty of Active Directory programming. The ADUtils class in my sample is based on a solution found in the book.


Reference
http://pholpar.wordpress.com/2010/08/10/synchronizing-user-image-between-active-directory-and-sharepoint-profile/
 

No comments:

Post a Comment