Wednesday, October 26, 2011

Custom Request Access page in Sharepoint 2010 - The Full Story

To share a quote from the most interesting man in the world... I don't code for Sharepoint often, but when I do, I prefer to know what the hell I am doing.

Update (06/06/2012): Before today the code that was here ran the possibility of generating a NullException error getting the current user. I have updated the code to resolve this based on information from Microsoft. For a descriptive addendum, see the update at the end of this post.

Recently I was tasked with customizing the Request Access page in Sharepoint 2010. Our group is small and our users not so saavy, so we simply wanted to list all the Site Owners on the Request Access page. This way end users would know who they needed to contact if they did not have access to a site. Slam dunk, right? Well, so I thought.

First off, I am by no means an expert Sharepoint or .NET developer. At the time of this writing, I have only developed a handful of Sharepoint applications and have under 6 months of experience. I have been a developer for a long time, but Sharepoint and .NET are not my native tongues. As such, I will explain as much as I understand and did to complete my tasks, but the why's of all of this will only be assumptions. Please feel free to educate me if I speak in error or you feel the need to clarify any of this.

I started out with a Google search, as all quests for knowledge usually do, and found a lot of links on how to do this. One of the most concise posts was by Anmol Rehan:

http://www.anmolrehan-sharepointconsultant.com/2011/08/how-to-use-custom-access-denied-page-in.html

One problem that is not well explained is that this creates a Site (Site Collection) scoped feature which will set the custom page. UpdateMappedPage() is not available to Sandboxed Solutions and must be a Farm solution, and sets the custom page at the WebApplication level. However, due to recent security fixes, you should get an error when trying to deploy or activate your solution.This is because webApp.Update() has to reach up into the WebApplication to work properly. As such, make sure you have this scoped at the WebApplication level and NOT the Site Collection level; Otherwise activating/deploying will give you an Access Denied error in your logs.

Another problem, my main problem and an issue with all the other information I found, is it only tell you how to configure Sharepoint to load a custom page. Anmol lists a simple 3 step process; however, step 2 was a lot more involved - at least for me - since this does not detail exactly on what you have to do to create these pages. I posted this question on Microsoft's forums (my post), but - as seems to be usual - I owned my own thread and had to figure it out myself. As my post started to be the number 1 hit when searching for my problem, I figured I should document this for others that might run into the same issue. This seems fairly common, but maybe I am just so green I am the only one that needed help. Either way, I hope this helps someone.

So look at Anmol's post and get started, when you get to step 2 "Create your costom Access Denied Page in Layouts" this is what you do.
  1. Right-click on your project in the Solution Explorer and pull out the menu for Add then click Sharepoint "Layouts" Mapped Folder.
  2. Right-click on the folder automatically created under the Layouts folder in your project then pull out the Add menu, click New Item and then select Application Page.
  3. Copy the contents original page into your custom one (these are located in the hive under TEMPLATE/LAYOUTS/) and go crazy. Easy right?
Well, not really. What I found is that if I just built my page then I would either get a 403 Forbidden error or got redirected to the Access Denied page. These types of pages are considers Safeguarded Application Pages, and appear to have some restrictions. First off, there can be issues with dynamic master pages and - I assume - other things that keep you from just laying into developing your custom page.

My first issue was I was really used to working with the code-behind; however, this only seemed to work if the Page directive Inherits itself. This also seemed to be the core reason for the 403 and Access Denied redirection. Once I changed this to inhert the original page - in my case Microsoft.SharePoint.ApplicationPages.RequestAccess - I could actually start seeing my changes. Of course, I could not use code-behind as any references to the page went all red and squiggly.

So you need to put all your functionality into the aspx page. Microsoft supported using code behind, but for these pages it seemed troublesome. This is a small bit of information, but this could have saved me a few days of work. I was never able to find any rules to what you could and could not do with these pages, and I would still love to know the hard facts. However, in the end I was able to get things working. We are still working around some issues related to using AD groups as Site Collection Administrators, which tends to list the AD group members as FullMask with the IsSiteAdmin equal to false, but I am hoping this is just an issue with our development environment.

To close up, this is the code I ended up using in my custom ReqAcc.aspx page:


Thanks to everyone for your support and help!

Update: For those that used the old version of the code (prior to 06/06/2012), here is the itemized changes:
  1. Replace SPUser reqUser = SPContext.Current.Web.CurrentUser; with string strUser = String.Format("{0}\\{1}", Environment.UserDomainName, Environment.UserName); and import the System namespace (<%@ Import Namespace="System" %>) You then need to convert this to a user object once you get into your RunWithElevatedPrivileges block with SPUser reqUser = thisroot.RootWeb.SiteUsers[strUser];. This is because the CurrentUser context was removed in these Safeguard Application pages as there was a bug allowing users to elevate privileges.
  2. I also now scope this as a WebApplication solution as restrictions were also put in place to disallow a Site Collection from updating the WebApplication. This makes sense, but limits us actually controlling this at a Site Collection.
  3. Added additional code I wrote for emailing site owners for access.

15 comments:

  1. Hi Umbrae,
    Thanks for sharing your code! I was looking for a better way to handle the Request Access page and I like your solution.
    Regarding the code-behind issue, I have a nearly full suite of error pages and all of them use code behind. The keys are 1) to create a SharePoint 2010 project for an Application page that inherits from the correct class - either the original page's class e.g. Microsoft.SharePoint.ApplicationPages.ErrorPage or the base class of that page; 2) for pages that inherit from UnsecuredLayoutsPageBase override the protected base class method AllowAnonymousAccess to always return true; 3) make sure that for Login, Signout, Error, ReqAcc, Confirmation, WebDeleted, and AccessDenied that you build your custom masterpage from simplev4.master; 4) deploy using a feature activation using code like Anmol Rehan's you link to above

    Having said that, I am having difficulty getting the ReqAcc page to work for some reason. It remains to be seen if these 4 keys really allow me to crack open this box.

    ReplyDelete
  2. Hah! Of course it helps to first enable the feature! (want-to-use-manage-access-requests-feature-in-sharepoint-2010? http://goo.gl/YZByR). Now I have to customize the Confirm.aspx as well ...

    ReplyDelete
  3. Thanks for the comments, Justin. I will definitely look into that.

    ReplyDelete
  4. You have saved me hours and hours of work (possibly resulting in not even finishing this).

    Thank you so much for sharing.

    ReplyDelete
  5. @Sean

    Glad I could help. That's why I posted it!

    As fyi I have been running into an issue with SPContext.Current.Web.CurrentUser returning null. Not sure if something happened in a patch or during an upgrade to 2008; however I will post something when I track it down.

    Obviously this only matters if you need the current user.

    ReplyDelete
  6. Yes, I'm also seeing SPContext.Current.Web.CurrentUser return null in one test environment, but not on my local dev machine. I'm not sure what's going on there, but it is frustrating.

    ReplyDelete
  7. Thanks, Sean. I am glad someone else is having the problem, so I can confirm I am not crazy. I am going to be picking this up again in the next few weeks and work with Microsoft to track it down. I will comment here and probably create a new post about it once I have a solution.

    ReplyDelete
    Replies
    1. What I did as a workaround in the mean time was pull the user account name out of the OOTB display control, <%$Resources:wss,reqacc_logonname%>, which puts the loginname of the user into the label: .

      You can get this value with javascript:
      document.getElementById('<%=LabelUserName.ClientID%>').innerHTML;

      and then pass it as a query string to the next page (or on refresh, etc), and then call SPWeb.EnsureUser on that value, which will return the SPUSer object you are looking for.

      This is not perfect, but it worked for my purposes. I hope it helps you in some way.

      Delete
    2. Sorry, the label - asp:Label ID="LabelUserName" runat="server"

      blogspot doesn't like HTML formatting :)

      Delete
    3. Thanks, Sean. Your workaround sounds like it would work, but my only concern would be that it is XSS vulnerable so additional validation on the url variable would need to be done in JS to avoid this.

      I am meeting with Microsoft this morning, so hopefully I will have more information on why SPContext.Current.Web.CurrentUser is returning null as well as their recommendation on how to accomplish this.

      Delete
  8. Updated today with recommendations from Microsoft. So far *knocks on wood* everything is working.

    ReplyDelete
  9. Hi Umbrae,

    Thanks for the info. I am a newbie to SP dev. Do you mind posting the event receiver code that shows the webapp scope? Thanks

    ReplyDelete
  10. Gene,

    The scope is configured on the Feature. So if you just double click on your Feature you should see a panel that has Title and Description and then a drop down for Scope. This is where you set this up for WebApplication, and I do not think this scoping actually shows in the scope.

    I did upload my Event Receiver to Gist if you want to see it:

    https://gist.github.com/3061132

    ReplyDelete
  11. Hi Umbrae,

    Thanks for the update. It is fixed now and looks great. A few comments... some enhancement considerations:

    1. Currently, the reqacc.aspx disply the name of the root site. It would be nice if it can display the actual site where the user is trying to get access to.

    2. In addition to the #1, if a user try to access a library (that has unique permission) which he has no access to, the reqacc.aspx still display the root site and the owners. It would be great if it displays the library/list/folder along with the owner of those objects. In other words, it would be much friendlier if it shows the actual object and its associated owner(s).

    Just my 2 cents. Thanks

    ReplyDelete
    Replies
    1. Thanks, Gene.

      It reports the name of the root site because our environment uses the MS recommendation of using Site collections. As such, we only care about the root site and few users have subsites. For our purposes, we needed this because users were not savvy to adjust the Request Access Email when people moved around in the org. If you make use of subsites (webs) instead of site collections then this code will definitely need some adjusting.

      Also, on our end we don't care about the lists either. Sort of the same issue as above. Very few of our users have special permissions on lists. Our goal was to introduce users to the site owners so our support group did not need to be involved.

      Good luck and I am glad this was helpful!

      Delete