Monday, April 16, 2012

Access SocialTerms or Tags in custom timer job

Share/Save/Bookmark


Though we thought this could make implementing the functionality very easy, it went through with a lot of research and lessons learnt.

So, what’s the issue?

When accessing the tags/terms(GetTerms) from SocialTagManager it always throw an error “Microsoft.Office.Server.UserProfiles.UserNotFoundException: An error was encountered while retrieving the user profile”. With a common sense we can see that timer jobs run in a different contextual process(owstimer.exe) which is by default configured to be run by networkservice account. So I thought maybe service accounts did not have user profiles and that’s why it threw error.
Now, I navigated to CA and tried to add user profile for networkservice account which threw another error. The whole point that I understand here is service accounts(system,networkservice) cannot have user profiles but only for domain accounts. [Lesson 0]

My common sense strikes again and asks why does it need a userprofile? I just asked to get all tags for a url on which service account cannot be used to Tag.
It’s time to review what MS has done. So I pulled the source revealer, Reflector from the ware house and found the below code in SocialTagManager class


public SocialTerm[] GetTerms(Uri url, int maximumItemsToReturn, SocialItemPrivacy socialItemPrivacy)

{
SocialTerm[] termArray2;
if (null == url)
{
throw new ArgumentNullException("url");
}

if ((maximumItemsToReturn < 0) || (maximumItemsToReturn > 0x3e8))
{
throw new ArgumentOutOfRangeException("maximumItemsToReturn");
}

bool flag = socialItemPrivacy == SocialItemPrivacy.IncludeAllPrivateData;

bool flag2 = socialItemPrivacy == SocialItemPrivacy.IncludeMyPrivateData;

if (flag && !base.IsSocialAdmin)
{
throw new UnauthorizedAccessException();
}

url = AlternateAccessMapping.GetSerializedUrl(url);
using (SqlCommand command = new SqlCommand("dbo.proc_SocialTags_GetTermsForUrl")) // Procedure to get all terms for url from DB
{

command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@includePrivate", SqlDbType.Bit).Value = flag;
if (flag2)
{
command.Parameters.Add("@viewer_user_recordID", SqlDbType.BigInt).Value = base.GetCurrentUserProfileRecordId(); // Incudes current user private data
}

…..

….

}

What we can infer from the code is that, when a user attempts to read social data his/her private data(tags/notes) is included the reason why GetTerms(url) is looking for userprofile of the user(networkservice) running the code - [Lesson1]. Found the root cause but what is the solution?

Quickly impersonation came to the mind, and I passed the token of my domain account while instantiating SPSIte object, still no luck but the same error. Again reflecting the DLL’ s I found the below from UserProfiles.UserProfileGlobal class.

[SecurityPermission(SecurityAction.Assert, Flags=SecurityPermissionFlag.ControlPrincipal
internal static string GetCurrentUserName()
{

string user = string.Empty;
HttpContext current = HttpContext.Current; //GOTCHA 1

if (current != null)
{
SPUtility.EnsureAuthentication(SPControl.GetContextWeb(current));

user = current.User.Identity.Name;
if (!current.User.Identity.IsAuthenticated)
{
throw new UnauthorizedAccessException(StringResourceManager.GetString("Exception_UserProfileGlobal_cs35"));
}
}

if ((user == null) || (user.Trim().Length == 0))
{
user = WindowsIdentity.GetCurrent().Name.ToLower(CultureInfo.InvariantCulture); //GOTCHA 2
}

if (!IsWindowsAuth())
{
if (!IsClaimsAuth())
{
user = SPUtility.FormatAccountName(user);
}

else
{
if (!SPClaimProviderManager.IsEncodedClaim(user))
{
user = SPUtility.FormatAccountName(ClaimsProviderConstants.strClaimsAuthMembershipProviderName, user);
}
user = EnsureWindowsLegacyAndClaimsEquivalence(user);
}
}

if (string.IsNullOrEmpty(user))
{
throw new UserProfileException(StringResourceManager.GetString("Exception_UserProfileGlobal_cs36"));
}
return user;
}

Ah, so that is how it’s getting the current user, which has always been the windows identity(code line labeled, GOTCHA 2) responsible for running the OWSTIMER process because HttpContext is always null in this case. As HttPContext is null, the impersonation code introduced by us will never gonna work. – [Lesson 2]
Since then I hated networkService and the object model for SocialData, being said that I could not wind this up concluding it’s not possible.

With no other option left, I have configured(SharePoint 2010 Timer->properties->logon Tab) my domain account(layman terms, account that has sharepoint userprofile) to run the OWSTIMER.EXE under services.msc. Restart timer service and IIS.

With no surprise, an error again but a new one, this time it threw the below error while instantiating SocailTagManager itself.

“Object reference could not be found at - at Microsoft.Office.Server.Administration.UserProfileApplicationProxy.get_ApplicationProperties().”

Even after using reflector, I could not find a clue but after gooogling, I came to know that I missed restarting the application service. [Lesson 3]

Now the error makes some sense for me. With an excitement I restarted User Profile Application service(CA->Manage Services on Server) and BRAVO, I nailed all the errors and it’s working. If you still did not get it working, you may need to try changing the account(in inetmgr) that runs the application pool responsible for User profile service application.

Though I found it working, I’m not sure on the best practice or the impact of configuring a non-service account to run the services.

UPDATE : Lately i learnt that socialterms can be accessed from timer job/console application(where httpcontext is null by default) by populating the context explicitly as below.

HttpRequest request = new HttpRequest("", http://teamsite, "");

HttpContext.Current = new HttpContext(request,
new HttpResponse(new StringWriter(CultureInfo.CurrentCulture)));
HttpContext.Current.Items["HttpHandlerSPWeb"] = web;

WindowsIdentity wi = WindowsIdentity.GetCurrent();
typeof(WindowsIdentity).GetField("m_name", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(wi, user.LoginName);
HttpContext.Current.User = new GenericPrincipal(wi, new string[0]);
WindowsIdentity wi2 = WindowsIdentity.GetCurrent();

SocialCommentManager socialCommentManager = new SocialCommentManager(
SPServiceContext.GetContext(HttpContext.Current));

Though i have not tried hands on, i see the above solution promising. Actual reference can be found here.
I appreciate MS for leaving such puzzles in the code ;) …LOL


Subscribe

No comments:

Post a Comment