Second Life of a Hungarian SharePoint Geek

February 19, 2012

Efforts taken to fool (or fix?) external data access security from the OWS timer process code

Based on the feedback I received to my last post, I think it would be useful to provide a bit more details about the different impersonation / context change methods I’ve tried to applied in my code when experimenting with external data access from OWS timer process. I will include a few helper methods in this post as well, for example one that can be used to trace out the current process context and identity details:

  1. private void TraceUserNames(string msg, SPWeb web)
  2. {
  3.     string httpContextUser = ((HttpContext.Current != null) && (HttpContext.Current.User != null) && (HttpContext.Current.User.Identity != null)) ?
  4.         HttpContext.Current.User.Identity.Name : string.Empty;
  5.     string windowsIdentity = (WindowsIdentity.GetCurrent() != null) ? WindowsIdentity.GetCurrent().Name : string.Empty;
  6.     string spUserName = ((web != null)) ? web.CurrentUser.LoginName : string.Empty;
  7.     Trace.TraceInformation("MailTestEventReceiver.ItemAdded, {3}. httpContextName: '{0}', windowsIdentity: '{1}', spUserName: '{2}'",
  8.        httpContextUser, windowsIdentity, spUserName, msg);
  9. }

You should include

using System.Diagnostics;

for the Trace commands. Using a method like this one is always suggested when you need to get information about what context your the process is running in.

The following code access the external list using the default context of the process:

  1. SPWeb web = properties.Web;
  2. HttpContext originalContext = HttpContext.Current;
  3.  
  4. TraceUserNames("Before impersonation", web);
  5. TestExtList(web);

In this case, TestExtList was a simple method to test external list access from code:

  1. private void TestExtList(SPWeb web)
  2. {
  3.     try
  4.     {
  5.         SPList extList = web.Lists[Global.ExtList];
  6.         SPListItemCollection items = extList.GetItems(extList.DefaultView);
  7.         Trace.TraceInformation("MailTestEventReceiver.TestExtList item count: {0}", items.Count);
  8.     }
  9.     catch (Exception ex)
  10.     {
  11.         Trace.TraceInformation("ERROR in MailTestEventReceiver.TestExtList: '{0}', '{1}'", ex.Message, ex.StackTrace);
  12.     }
  13. }

When uploading documents from the UI, the result of the TraceUserNames method:

Before impersonation. httpContextName: ”, windowsIdentity: ‘domain\user’, spUserName: ‘domain\user’

However, when the code is triggered by an incoming mail, the result is pretty different:

Before impersonation. httpContextName: ”, windowsIdentity: ‘domain\farmAdmin’, spUserName: ‘SHAREPOINT\system’

That is true, even if the E-mail security policy at Incoming e-mail settings is set to Accept e-mail messages based on document library permissions (A).

image

So this setting seems to have no effect on the context identity.

The differences I found between this one and Accept e-mail messages from any sender (B).

  1. In the first case (A) a user is looked up based on the e-mail address, and if no one is found or this user has no write permission, the mail won’t be delivered to the document library. In the case (B) the mail is delivered without this kind of verification.
  2. In the case (A) Created By / Modified By fields are set to the user determined by the e-mail address. In the case (B) theses fields are set to SHAREPOINT\system. This latter one correlates with the SharePoint user identity I’ve found for the incoming mail process (see above).

Note: It means that no really security check / authorization happens here. The SMTP mails required no authentication, and it is not so complex to fake a mail with a sender mail address that has write access to the library.

As you may remember from the former post, accessing the external data from code was successful when the document was uploaded from the browser, however it failed with the exception below, when sent as a mail attachment to the library.

Access denied by Business Data Connectivity.
at Microsoft.SharePoint.SPListDataSource.GetEntityInstanceEnumerator(XmlNode xnMethodAndFilters)
at Microsoft.SharePoint.SPListDataSource.GetFilteredEntityInstancesInternal(XmlDocument xdQueryView, Boolean fFormatDates, Boolean fUTCToLocal, String firstRowId, Boolean fBackwardsPaging, String& bdcidFirstRow, String& bdcidNextPageRow, List`1& lstColumnNames, Dictionary`2& dictColumnsUsed, List`1& mapRowOrdering, List`1& lstEntityData)
at Microsoft.SharePoint.SPListDataSource.GetFilteredEntityInstances(XmlDocument xdQueryView, Boolean fFormatDates, Boolean fUTCToLocal, String firstRowId, Boolean fBackwardsPaging, String& bdcidFirstRow, String& bdcidNextPageRow, List`1& lstColumnNames, Dictionary`2& dictColumnsUsed, List`1& mapRowOrdering, List`1& lstEntityData)
at Microsoft.SharePoint.SPListItemCollection.EnsureEntityDataViewAndOrdering(String& bdcidFirstRow, String& bdcidNextPageFirstRow)
at Microsoft.SharePoint.SPListItemCollection.EnsureListItemsData()
at Microsoft.SharePoint.SPListItemCollection.get_Count()

To fix this, I first tried to use “simple” elevated permissions:

  1. Guid siteId = properties.SiteId;
  2. Guid webId = properties.Web.ID;
  3.  
  4. SPSecurity.RunWithElevatedPrivileges(delegate()
  5.     {
  6.         using (SPSite siteImp = new SPSite(siteId))
  7.         {
  8.             using (SPWeb webImp = siteImp.OpenWeb(webId))
  9.             {
  10.                 TraceUserNames("Using elevated privileges", webImp);
  11.                 TestExtList(webImp);
  12.             }
  13.         }
  14.     });

The output of the TraceUserNames method was:

Using elevated privileges. httpContextName: ”, windowsIdentity: ‘domain\farmAdmin’, spUserName: ‘SHAREPOINT\system’

The exception on external data access was in this case:

Attempted to perform an unauthorized operation.
at Microsoft.SharePoint.SPListDataSource.CheckUserIsAuthorized(SPBasePermissions perms)
at Microsoft.SharePoint.SPListDataSource.GetFilteredEntityInstances(XmlDocument xdQueryView, Boolean fFormatDates, Boolean fUTCToLocal, String firstRowId, Boolean fBackwardsPaging, String& bdcidFirstRow, String& bdcidNextPageRow, List`1& lstColumnNames, Dictionary`2& dictColumnsUsed, List`1& mapRowOrdering, List`1& lstEntityData)
at Microsoft.SharePoint.SPListItemCollection.EnsureEntityDataViewAndOrdering(String& bdcidFirstRow, String& bdcidNextPageFirstRow)
at Microsoft.SharePoint.SPListItemCollection.EnsureListItemsData()
at Microsoft.SharePoint.SPListItemCollection.get_Count()

My next thought was that I should probably apply some kind of impersonation to reproduce the context I found when uploading the document from the browser (see the first TraceUserNames output above).

To get the user I should to impersonate, I read the value of the Modified By field (as discussed earlier, this field contains the sender in scenario A).

  1. SPListItem item = properties.ListItem;
  2. Object editorFieldValueRaw = item[SPBuiltInFieldId.Editor];
  3. SPFieldUserValue editorFieldValue = (editorFieldValueRaw is String) ?
  4.     new SPFieldUserValue(web, (String)editorFieldValueRaw) :
  5.     (SPFieldUserValue)editorFieldValueRaw;
  6. SPUser editor = editorFieldValue.User;
  7. Trace.TraceInformation("User to impersonate: '{0}'", editor.LoginName);

The following code used to impersonate the SharePoint user:

  1. using (SPSite siteImp = new SPSite(siteId, editor.UserToken))
  2. {
  3.     using (SPWeb webImp = siteImp.OpenWeb(webId))
  4.     {
  5.         TraceUserNames("After impersonation", webImp);
  6.         TestExtList(webImp);
  7.     }
  8. }

The output of the TraceUserNames method was:

After impersonation. httpContextName: ”, windowsIdentity: ‘domain\farmAdmin’, spUserName: ‘domain\user’

I’ve received the original Access Denied exception when tried to get the number of item in the external list.

Next, I’ve injected a HttpContext as described in this and this posts.

  1. IPrincipal impersonationPrincipal = new WindowsPrincipal(new WindowsIdentity(GetUpn(editor)));
  2. HttpRequest request =
  3. new HttpRequest(string.Empty, properties.WebUrl, string.Empty);
  4.  
  5. HttpContext originalContext = HttpContext.Current;
  6.  
  7. HttpResponse response = new HttpResponse(
  8.      new System.IO.StreamWriter(new System.IO.MemoryStream()));
  9.  
  10. HttpContext impersonatedContext = new HttpContext(request, response);
  11. // these lines required to inject SPContext as well
  12. // if you don't need that it can be deleted
  13. impersonatedContext.User = impersonationPrincipal;
  14. if (web != null)
  15. {
  16.     impersonatedContext.Items["HttpHandlerSPWeb"] = web;
  17. }
  18. HttpContext.Current = impersonatedContext;
  19.  
  20. TraceUserNames("Dummy HTTP context", web);
  21. TestExtList(web);
  22.  
  23. HttpContext.Current = originalContext;

The output of the TraceUserNames method was:

Dummy HTTP context. httpContextName: ‘domain\user’, windowsIdentity: ‘domain\farmAdmin’, spUserName: ‘SHAREPOINT\system’

Again, I’ve received the original Access Denied exception.

Finally, I’ve applied Windows impersonation (after granting the Act as part of the operating system user right to the SharePoint 2010 Timer service identity):

  1. WindowsImpersonationContext impersonationContext = null;
  2. try
  3. {
  4.     WindowsIdentity userIdentity = new WindowsIdentity(GetUpn(editor));
  5.     impersonationContext = userIdentity.Impersonate();
  6.  
  7.     using (SPSite siteImp = new SPSite(siteId))
  8.     {
  9.         using (SPWeb webImp = siteImp.OpenWeb(webId))
  10.         {
  11.             TraceUserNames("After Windows impersonation", webImp);
  12.             TestExtList(webImp);
  13.         }
  14.     }
  15.  
  16. }
  17. catch (Exception ex)
  18. {
  19.     Trace.TraceInformation("ERROR in MailTestEventReceiver.ItemAdded impersonation: '{0}', '{1}'", ex.Message, ex.StackTrace);
  20. }
  21. finally
  22. {
  23.     if (impersonationContext != null)
  24.     {
  25.         impersonationContext.Undo();
  26.     }
  27. }

The GetUpn method used in the former code (using System.DirectoryServices.ActiveDirectory namespace is required):

  1. private static string GetUpn(SPUser spUser)
  2. {
  3.     string[] userName = spUser.LoginName.Split('\\');
  4.     Domain domain = Domain.GetCurrentDomain();
  5.     string upn = userName[1] + "@" + domain.Name;
  6.     Trace.TraceInformation("MailTestEventReceiver.GetUpn result: '{0}'", upn);
  7.     return upn;
  8. }

The output of the TraceUserNames method was:

After Windows impersonation. httpContextName: ”, windowsIdentity: ‘domain\user’, spUserName: ‘SHAREPOINT\system’

Again, I’ve received the original Access Denied exception.

As you can see, none of this methods resulted the same output as the one from the user interface-based upload, however mixing the methods (Windows + SharePoint impersonation) the output was the same. However, it did not help to avoid the exception, so the problem seemed to be a little more complex.

After introducing a workaround in the last post, in the next post I will show you a solution for this issue.

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: