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…
…and set its Root application partition property to the root of your AD.
Assuming there is no user photo in the profile nor in the AD, you should see something similar first:
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:
If you press the >> button, the image is transferred to AD.
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.
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:
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.
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.
When you click on the >> button to copy SharePoint profile image to AD the following code is called:
Similarly, when you click << to copy image from AD to the SharePoint profile, the following code gets executed:
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):
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/
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…
…and set its Root application partition property to the root of your AD.
Assuming there is no user photo in the profile nor in the AD, you should see something similar first:
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:
If you press the >> button, the image is transferred to AD.
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.
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:
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.
- [Personalizable(),
- WebBrowsable(),
- WebDisplayName("Root application partition")]
- public string DefaultPartition
- {
- get;
- set;
- }
- #endregion
- protected override void CreateChildControls()
- {
- ProfilePhotoSyncWebPartUserControl control = (ProfilePhotoSyncWebPartUserControl)Page.LoadControl(_ascxPath);
- control.DefaultPartition = DefaultPartition;
- Controls.Add(control);
- }
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.
- private void RefreshControls()
- {
- String userName = Request.QueryString["UserName"];
- // hack: render response as image for AD "thumbnailPhoto" image
- if (!String.IsNullOrEmpty(userName))
- {
- Response.Clear();
- ADUtils adUtils = new ADUtils(DefaultPartition);
- using (DirectoryEntry root = adUtils.GetDefaultPartition())
- using (DirectoryEntry user = adUtils.FindUserByAccountName(userName))
- {
- byte[] thumbnailPhotoBytes = (byte[])user.Properties["thumbnailPhoto"].Value;
- if (thumbnailPhotoBytes != null)
- {
- Response.BinaryWrite(thumbnailPhotoBytes);
- }
- }
- Response.End();
- }
- else
- {
- try
- {
- ADUtils adUtils = new ADUtils(DefaultPartition);
- using (DirectoryEntry root = adUtils.GetDefaultPartition())
- using (DirectoryEntry user = adUtils.FindUserByAccountName(_shortName))
- {
- byte[] thumbnailPhotoBytes = (byte[])user.Properties["thumbnailPhoto"].Value;
- bool hasThumbnailPhoto = (thumbnailPhotoBytes != null);
- ADImage.Visible = hasThumbnailPhoto;
- ADImageLabel.Visible = !hasThumbnailPhoto;
- if (hasThumbnailPhoto)
- {
- ADImage.ImageUrl = String.Format("{0}?UserName={1}", Request.Url.GetLeftPart(UriPartial.Path), _shortName);
- }
- }
- }
- catch (Exception ex)
- {
- Warning.Visible = true;
- }
- UserProfileManager userProfileManager = new UserProfileManager(SPServiceContext.Current);
- UserProfile userProfile = userProfileManager.GetUserProfile(_accountName);
- String pictureUrl = (String)userProfile["PictureUrl"].Value;
- bool hasProfilelPhoto = (!String.IsNullOrEmpty(pictureUrl));
- // double check to be sure the file is there to avoid "missing" images on the page
- // when removing the profile image on the UI it takes some time the change get reflected
- // in the profile property, but image is deleted immediately
- if (hasProfilelPhoto)
- {
- using (SPSite site = new SPSite(pictureUrl))
- {
- using (SPWeb web = site.OpenWeb())
- {
- SPFile file = web.GetFile(pictureUrl);
- hasProfilelPhoto = file.Exists;
- }
- }
- }
- SPImage.Visible = hasProfilelPhoto;
- SPImageLabel.Visible = !hasProfilelPhoto;
- if (hasProfilelPhoto != null)
- {
- SPImage.ImageUrl = pictureUrl;
- }
- }
- }
- protected void SP2AD_Click(object sender, EventArgs e)
- {
- UserProfileManager userProfileManager = new UserProfileManager(SPServiceContext.Current);
- UserProfile userProfile = userProfileManager.GetUserProfile(_accountName);
- String pictureUrl = (String)userProfile["PictureUrl"].Value;
- bool hasProfilelPhoto = (!String.IsNullOrEmpty(pictureUrl));
- byte[] imageContent = null;
- // double check to be sure the file is there to avoid "missing" images on the page
- // when removing the profile image on the UI it takes some time the change get reflected
- // in the profile property, but image is deleted immediately
- if (hasProfilelPhoto)
- {
- using (SPSite site = new SPSite(pictureUrl))
- {
- using (SPWeb web = site.OpenWeb())
- {
- SPFile file = web.GetFile(pictureUrl);
- if (file.Exists)
- {
- imageContent = file.OpenBinary();
- }
- }
- }
- }
- ADUtils adUtils = new ADUtils(DefaultPartition);
- using (DirectoryEntry root = adUtils.GetDefaultPartition())
- using (DirectoryEntry user = adUtils.FindUserByAccountName(_shortName))
- {
- user.Properties["thumbnailPhoto"].Clear();
- if (imageContent != null)
- {
- user.Properties["thumbnailPhoto"].Add(imageContent);
- }
- user.CommitChanges();
- }
- RefreshControls();
- }
- protected void AD2SP_Click(object sender, EventArgs e)
- {
- try
- {
- ADUtils adUtils = new ADUtils(DefaultPartition);
- using (DirectoryEntry root = adUtils.GetDefaultPartition())
- using (DirectoryEntry user = adUtils.FindUserByAccountName(_shortName))
- {
- byte[] thumbnailPhotoBytes = (byte[])user.Properties["thumbnailPhoto"].Value;
- bool hasThumbnailPhoto = (thumbnailPhotoBytes != null);
- ADImage.Visible = hasThumbnailPhoto;
- ADImageLabel.Visible = !hasThumbnailPhoto;
- if (hasThumbnailPhoto)
- {
- using (SPSite site = new SPSite(GetMySiteHostUrl(SPContext.Current.Site)))
- {
- site.AllowUnsafeUpdates = true;
- using (SPWeb web = site.OpenWeb())
- {
- web.AllowUnsafeUpdates = true;
- ProfileImagePicker profileImagePicker = new ProfileImagePicker();
- InitializeProfileImagePicker(profileImagePicker, web);
- SPFolder subfolderForPictures = GetSubfolderForPictures(profileImagePicker);
- UploadPhoto(_accountName, thumbnailPhotoBytes, subfolderForPictures);
- SetPictureUrl(_accountName, subfolderForPictures);
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- Warning.Visible = true;
- }
- RefreshControls();
- }
Active Directory related code is included in the ADUtils class (see credits later):
- public class ADUtils
- {
- public ADUtils(String defaultPartition)
- {
- DefaultPartition = defaultPartition;
- }
- private String DefaultPartition
- {
- get;
- set;
- }
- /// <summary>
- /// Retrieves a DirectoryEntry using configuration data
- /// </summary>
- /// <param name="path"></param>
- /// <returns></returns>
- public DirectoryEntry CreateDirectoryEntry(string path)
- {
- return new DirectoryEntry(String.Format("LDAP://{0}", path));
- }
- /// <summary>
- /// Creates a DirectoryEntry from the DefaultPartition defined in config
- /// </summary>
- /// <returns></returns>
- public DirectoryEntry GetDefaultPartition()
- {
- return CreateDirectoryEntry(DefaultPartition);
- }
- /// <summary>
- /// Simple method to find and return a user using the CN name and searching the
- /// defaultNamingContext defined in config.
- /// </summary>
- /// <param name="userRDN"></param>
- /// <returns></returns>
- public DirectoryEntry FindUserByCN(string userRDN)
- {
- using (DirectoryEntry searchRoot = GetDefaultPartition())
- {
- DirectorySearcher ds = new DirectorySearcher(
- searchRoot,
- String.Format("(cn={0})", userRDN),
- new string[] { "cn" },
- SearchScope.Subtree
- );
- SearchResult sr = ds.FindOne();
- return (sr != null) ? sr.GetDirectoryEntry() : null;
- }
- }
- /// <summary>
- /// Simple method to find and return a user using the sAMAccountName name and searching the
- /// defaultNamingContext defined in config.
- /// </summary>
- /// <param name="userRDN"></param>
- /// <returns></returns>
- public DirectoryEntry FindUserByAccountName(string accountName)
- {
- Trace.TraceInformation("FindUserByAccountName method called. Account name: '{0}'", accountName);
- using (DirectoryEntry searchRoot = GetDefaultPartition())
- {
- DirectorySearcher ds = new DirectorySearcher(
- searchRoot,
- String.Format("(sAMAccountName={0})", accountName),
- new string[] { "sAMAccountName" },
- SearchScope.Subtree
- );
- SearchResult sr = ds.FindOne();
- Trace.TraceInformation("FindUserByAccountName user found. Path: '{0}'", sr.Path);
- return (sr != null) ? sr.GetDirectoryEntry() : null;
- }
- }
- }
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