Tuesday, May 11, 2010

Making Linq to SharePoint work for Anonymous users

Earlier today I ran into the issue of Linq to SharePoint not working for anonymous users. The issue is discussed at a number of places:


http://blog.mastykarz.nl/sharepoint-2010-linq-doesnt-support-anonymous-users/


http://social.technet.microsoft.com/Forums/en-US/sharepoint2010programming/thread/9b59abcb-6bce-42f1-9eae-ad9561753044


Seeing as 95% of the SharePoint work I do is on public facing web sites, this was a real disappoinment for me. SharePoint 2010 is supposed to be much more useable internet sites - so this was a bit of a shock. Linq to SharePoint was one of those features we all looked forward to!


Before going back to CAML queries, I thought I would have a look at what potential workarounds or even hacks I could come up with to get this working.


My first try was to just wrap my code in the usual SPSecurity.RunWithElevatedPrivileges method. That didn't work. A number of experiments later (and then finding this post), I arrived at this picture form Reflector:



The class we are looking at here is responsible for creating the data connection for the Microsoft.SharePoint.Linq.DataContext class. The important thing to notice here is the highlighted line. If the SPContext.Current is not null, the code uses the SPSite object from the current SPContext.


As documented all over the web, the RunWithElevatedPrivileges method will be of no help if you do not create new SP* objects. See http://msdn.microsoft.com/en-us/library/bb466220.aspx "...You cannot use the objects available through the Microsoft.SharePoint.SPContext.Current property. That is because those objects were created in the security context of the current user..."


So since the code shown in reflector does exactly what the above mentioned article warns against, browsing your site as an anonymous user eventually causes a login prompt on your site since the current SPContext represents that anonymous user who rightly should NOT be able to run Linq queries against your database.


Next I was inspired by this post and thought: What if I can somehow mess with the SPContext.Current object? If I could get it to be null, I could force that code in the SPServerDataConnection class to create a new SPSite object!


The SPContext.Current object is read only. However, it derives in one way or another from the HttpContext.Current, and that is writeable. So my next attempt was to check if my user was an anonymous user, and if so, set the HttpContext to null. Short story - it worked!


After some cleanup, I created the following helper method:

public static class AnonymousContextSwitch
{
public static void RunWithElevatedPrivelligesAndContextSwitch(SPSecurity.CodeToRunElevated secureCode)
{
try
{
//If there is a SPContext.Current object and there is no known user, we need to take action
bool nullUser = (SPContext.Current != null && SPContext.Current.Web.CurrentUser == null);

HttpContext backupCtx = HttpContext.Current;
if (nullUser)
HttpContext.Current = null;

SPSecurity.RunWithElevatedPrivileges(secureCode);

if (nullUser)
HttpContext.Current = backupCtx;
}
catch (Exception ex)
{
string errorMessage = "Error running code in null http context";
//Use your favourite form of logging to log the error message and exception ....
}
}
}


The logic is as follows:

  1. Check if the situation requires action: Is the user anonymous?

  2. Backup the current HttpContext

  3. Set the current HttpContext to null - thus forcing the creation of new SP* objects

  4. Use The RunWithElevatedPrivileges method to execute code specified by the caller. Note that I reuse the SPSecurity.CodeToRunElevated delegate to mimic the RunWithElevatedPrivileges method.

  5. Set the current HttpContext to the backed up object


Calling this function is identical to the way RunWithElevatedPrivileges is called:

string currentWebUrl = SPContext.Current.Web.Url;
AnonymousContextSwitch.RunWithElevatedPrivelligesAndContextSwitch(
delegate
{
MyDataContext dctx = new MyDataContext(currentWebUrl);
//... your code...
});


Just remember that the SPContext.Current object should NOT be referenced inside the delegate code. This will throw all sorts of Null Reference exceptions. If you need data such as the current web url, throw it in a string variable before you switch the context. See the example above.


I have tested this code for retrieving as well as updating data in a list and it worked great. I have also tested this for a logged in as well as an anonymous user and it seems to work for both.


Lastly, I need to say that I just figured this out today and I have NO idea what the long term impact of this will be, or if it will work as expected in all cases. Use at your own risk and all that. Happy coding!

8 comments:

André Rentes said...

Hi Joe,

Thanks for this post, I tried to do it and worked like a charm!

Note that this is a limitation that may be resolved in futere as we can see:

http://msdn.microsoft.com/en-us/library/ff798485.aspx

Congrats!

Joe said...

Thanks for the link Andre, this is interesting to see. I guess MS realized the fix shouldn't be too difficult. Let's hope they aren't just teasing us.

Jiveabillion said...

This doesn't work for me. I get this error "This control only works in the context of a SharePoint site. SPContext.GetContext failed to return an SPContext for the current request context." When I try to use your code in a web part. It seems like it errors out on the ribbon control.

[InvalidOperationException: This control only works in the context of a SharePoint site. SPContext.GetContext failed to return an SPContext for the current request context.]
Microsoft.SharePoint.Utilities.RightsSensitiveVisibilityHelper.UserHasRights(PermissionContext permissionContext, SPBasePermissions permissions, PermissionMode permissionMode, SPContext context, SPWeb contextWeb, SPList contextList) +22147174
Microsoft.SharePoint.Utilities.RightsSensitiveVisibilityHelper.ShouldBeVisible(PermissionContext permissionContext, SPBasePermissions permissions, PermissionMode permissionMode, PageModes pageModes, AuthenticationRestrictions authenticationRestrictions, SPContext renderContext, SPList contextList) +37
Microsoft.SharePoint.WebControls.SPRibbon.get_ShouldRender() +211
Microsoft.SharePoint.WebControls.SPRibbon.OnPreRender(EventArgs e) +64
System.Web.UI.Control.PreRenderRecursiveInternal() +108
System.Web.UI.Control.PreRenderRecursiveInternal() +224
System.Web.UI.Control.PreRenderRecursiveInternal() +224
System.Web.UI.Control.PreRenderRecursiveInternal() +224
System.Web.UI.Control.PreRenderRecursiveInternal() +224
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +3394

00jujster said...

Just want to thank you for a lovely work around to a rather big problem. Keep up the good work. Thanks

dee said...

Thanks for this post it worked perfectly.

Regards
Damien

Unknown said...

It's nice to have an alternative but I think this is still not the final solution. Running code with elevated privileges means user can see everything, imagine an application that is based on permissions (most of SharePoint apps are) and also the case where you have workflow approval content (in lists, libraries etc) then running code with elevated privilege means user may "see" data that he is not supposed to.

Joe said...

@Unknown, how you use the RunWithElevatedPriviliges method call is a different matter from the HttpContext workaround described here. The RunWithElevatedPriviliges can be abused regardless of using LINQ or not, and it is up to the developer to determine what they are doing inside of this code.
Also, most SharePoint solution that are base on permissions will likely not be dealing much with the anonymous user. Remember that this scenario was only meant to workaround a bug in the case of anonymous users.
Lastly, you are 100% correct that this is not a final solution. Therefore, and I have not had a chance to test this yet, there is a fix out for this in the August 2010 CU which is also part of SharePoint 2010 SP1. So in essence, this post is irrelevant if you upgrade your SharePoint server. (Again, haven't tested this yet)
Hope that clears things up a bit.

Kleber Aquino said...

Muito obrigado!!!

Resolveu o meu problema!!!

Parabens!!!

Abraços