Sunday, April 17, 2011

Access Denied With Linq to SharePoint (SPMetal)

Access Denied

SPMetal (Linq to SharePoint) is kind of a mixed bag. On one had it’s great addition to what used to be a very anemic toolset for SharePoint. On the other hand it has some pretty remarkable shortcomings that aren’t really advertised all that well in the documentation.
Two of the more notable ones:
  • No support for anonymous users (at least at the time of writing)
  • No support for RunWithElevatedPrivileges
These two shortcomings are actually very intimately related. If you’re using SPMetal (Linq to SharePoint) and you find that code inAccess Denied when trying to RunWithElevatedPrivileges RunWithElevatedPrivileges still yields Access Denied errors (or doesn’t seem to run in any kind of elevated context) you’ve potentially encountered a very awkward shortcoming of SPMetal.
The good news is that there are some very functional workarounds to these issues but for you to troubleshoot these (and discern where they’re appropriate) it helps to know why these errors happen.

What’s Going On?

For those who have worked with RunWithElevatedPrivileges before you know that certain “shapes” of code don’t work. An example of this would be running some code with RunWithElevatedPrivileges context but then using an object you’d previously created outside the elevated context. Here are two examples of RunRunWithElevatedPrivileges, one which still throws an Access Denied and another that works.
The astute observer will recognize that the below will throw an Access Denied error
//Doesn't work, still yields access denied.
SPSecurity.RunWithElevatedPrivileges(delegate()
{
  using (SPSite site = SPContext.Current.Site)
  {
    using (SPWeb web = site.RootWeb)
    {
      web.AllowUnsafeUpdates = true;
      SPList list = web.Lists["ListName"];
      SPListItem item = list.AddItem();
      item["Property"] = "Property Value";
      item.Update();
    }
  }
});

The reason the above fails is that the SPContext.Current.Site gets created relatively early in the request lifecycle. By the time you call it from some page/webpart/etc… the SPSite object has long since been constructed, and at the time the SPSite WAS constructed, the application was NOT running in a RunWithElevatedPrivileges security context.

The below DOES work since a new SPSite (and all other objects) are created in the RunWithElevatedPrivileges security context.

//Works because the SPSite, SPWeb etc... are created in the new elevated security context.
SPSecurity.RunWithElevatedPrivileges(delegate()
{
  using (SPSite site = new SPSite("http://servername"))
  {
    using (SPWeb web = site.RootWeb)
    {
      web.AllowUnsafeUpdates = true;
      SPList list = web.Lists["ListName"];
      SPListItem item = list.AddItem();
      item["Property"] = "Property Value";
      item.Update();
    }
  }
});

The moral of the story is that objects you’re working with need to be recreated within your RunWithElevatedPrivileges code block.

The Problem with SPMetal

The two problems we kicked off this post with:

  • No support for anonymous users (at least at the time of writing)
  • No support for RunWithElevatedPrivileges

Actually stem from the same class of problem as the incorrect “shape” of code shown in the RunWithElevatedPrivileges example above. When the Linq to SharePoint Provider goes to create a connection to the given SPSite/SPWeb, it’ll take the current SPSite object out of the SPContext.Current context. The problem with this is that there’s no (easy) way to have that SPSite object constructed with an elevated security context.


Here’s a look at the reflected code.
SPServerDataConnection.png

The solution that most people have taken for the anonymous access issues also works for the RunWithElevatedPrivileges issues.

The trick is to set the HttpContext to null temporarily just prior to creating creating the DataContext in a RunWithElevatedPrivileges block. This bullies the Linq to SharePoint provider into creating a new SPSite, and this one is created with the correct context.

Below is a helper method which follows the RunWithElevatdPrivileges pattern which allows you to create a DataContext that will indeed run with elevated privileges.

/// <summary>
/// Runs with elevated priviledges after setting the HttpContext temporarily to null prior to doing so. This allows linq to sql 
/// to create a DataContextManager based on the RunWithElevatedCredential (farm account) instead of the current user.
/// Otherwise the linq2sql DataContextManager would take its SPSite off of the SPContext.Current.Site which is already cached
/// with the priviledges of the current user.
/// 
/// NOTE: This should ONLY BE CALLED by code creating Linq to SharePoint SharePointDataContexts. It's also of note that any
/// SharePointDataContext created using this code WILL BE ABLE TO DO ANYTHING the farm account can.
/// </summary>
protected static void RunWithElevatedPrivilegesAndContextSwitch(SPSecurity.CodeToRunElevated secureCode)
{
  HttpContext backupContext = HttpContext.Current;

  HttpContext.Current = null;
  SPSecurity.RunWithElevatedPrivileges(secureCode);

  HttpContext.Current = backupContext;
}
Usage might look something like the following:

SharePointDataContext context = null;
string url = SharePointHelper.WebApplicationRoot;

RunWithElevatedPrivilegesAndContextSwitch(delegate()
{
  context = new SharePointDataContext(url);
});

//Do something you normally wouldn’t be able to with this elevated linq to SharePoint context.
List<ListName> items = context.ListName.ToList();

It’s worth mentioning that if your data access is consolidated in to a formal data access layer you may need to centralize this kind of code so that it doesn’t get placed all over the site, but above is the central idea behind getting Linq to Sharepoint DataContexts to run with Farm Account privileged access.


I hope the above makes sense, and helps.


My Best,
Tyler