Elegant SPSite Elevation

A common difficulty with SPSecurity.RunWithElevatedPrivileges (most recently lamented here) is that existing SPSite/SPWeb objects will not work with the elevated security context. The natural workaround is to create a new SPSite within the elevated delegate and off you go. However, Dan Larson has documented a better technique here, leveraging the SPSite constructor that accepts an SPUserToken. By passing the token of the system account (SHAREPOINT\system, an internal alias for the application pool identity), an elevated site is available without the complexity and complications of RunWithElevatedPrivileges.

With the emerging best practice of separating SPSite and SPWeb creation from the code that uses them, I have expanded on Dan’s technique with another series of extension methods. The first step is to get the system token from an existing SPSite, adapted from Dan’s example:

public static SPUserToken GetSystemToken(this SPSite site)
{
    SPUserToken token = null;
    bool tempCADE = site.CatchAccessDeniedException;
    try
    {
        site.CatchAccessDeniedException = false;
        token = site.SystemAccount.UserToken;
    }
    catch (UnauthorizedAccessException)
    {
        SPSecurity.RunWithElevatedPrivileges(() =>
        {
            using (SPSite elevSite = new SPSite(site.ID))
                token = elevSite.SystemAccount.UserToken;
        });
    }
    finally
    {
        site.CatchAccessDeniedException = tempCADE;
    }
    return token;
}

And then we simply provide methods to operate on an elevated site created with that token:

public static void RunAsSystem(this SPSite site, Action<SPSite> action)
{
    using (SPSite elevSite = new SPSite(site.ID, site.GetSystemToken()))
        action(elevSite);
}

public static T SelectAsSystem<T>(this SPSite site, Func<SPSite, T> selector)
{
    using (SPSite elevSite = new SPSite(site.ID, site.GetSystemToken()))
        return selector(elevSite);
}

Note that the object(s) returned by selector should not be SharePoint objects that hold references to a parent site/web.

It’s also useful to provide additional methods that simplify working with elevated webs:

public static void RunAsSystem(this SPSite site, Guid webId, Action<SPWeb> action)
{
    site.RunAsSystem(s => action(s.OpenWeb(webId)));
}

public static void RunAsSystem(this SPSite site, string url, Action<SPWeb> action)
{
    site.RunAsSystem(s => action(s.OpenWeb(url)));
}

public static void RunAsSystem(this SPWeb web, Action<SPWeb> action)
{
    web.Site.RunAsSystem(web.ID, action);
}

public static T SelectAsSystem<T>(this SPSite site, Guid webId, Func<SPWeb, T> selector)
{
    return site.SelectAsSystem(s => selector(s.OpenWeb(webId)));
}

public static T SelectAsSystem<T>(this SPSite site, string url, Func<SPWeb, T> selector)
{
    return site.SelectAsSystem(s => selector(s.OpenWeb(url)));
}

public static T SelectAsSystem<T>(this SPWeb web, Func<SPWeb, T> selector)
{
    return web.Site.SelectAsSystem(web.ID, selector);
}

I don’t bother to Dispose the webs created by these calls to s.OpenWeb() because I know s will be disposed shortly, cleaning up the web with it.

Usage

As an example, let’s refactor Nigel’s skeleton code:

protected override void CreateChildControls()
{
    SPWeb web = SPControl.GetContextWeb(Context);
    web.RunAsSystem(UpdateVisitorList);
}

private void UpdateVisitorList(SPWeb web)
{
    SPList visList = web.Lists[_visitorList];
    int existItemId = GetVisitorItemId(visList);

    web.AllowUnsafeUpdates = true;
    if (existItemId < 1)
    {
        SPListItem newItem = visList.Items.Add();
        // Set field values
        newItem.Update();
    }
    else
    {
        SPListItem oldItem = visList.GetItemById(existItemId);
        // Update field values
        oldItem.Update();
    }
    web.AllowUnsafeUpdates = false;
}

By separating the elevated operation from the logic required to elevate, the code is much easier to read and less prone to errors like leaking an elevated SPSite.

About these ads

20 Responses to “Elegant SPSite Elevation”

  1. Nigel Witherdin Says:

    Hi Keith,

    This seem a much cleaner and safer way to achieve the elevated privileges to me. Will look at refactoring my code, and adding the “RunAsSystem” funtionality to my SharePoint.Common library asap :)

    Many thanks for the tips!

    Regards,
    Nigel

  2. Alex Says:

    Keith,

    this is really elegant! I am new to extensions, and hand problems in getting the SelectAsSystem extension to work. I got some compiler errors on my first attempt, and finally came up with this solution:

    SPSite site = SPContext.Current.Site;
    string result = site.SelectAsSystem((Func<SPSite, string>)SomeFunction);

    public string SomeFunction(SPSite site)
    {

    }

    I’m not sure if this would be considered as elegant… ;-)
    Do you have a better solution?

    Thanks in advance!
    Alex

    • Keith Dahlby Says:

      Hi Alex ~

      There are a number of compiler errors that you could get when working with lambdas. Maybe not all code paths returned an object? Maybe the type of the returned object couldn’t be inferred? Did you name the delegate’s argument “site”, which would conflict with the previous definition of site?

      This refactoring of your code should work fine, assuming the error isn’t coming from “…”:

      SPSite contextSite = SPContext.Current.Site;
      string result = contextSite.SelectAsSystem(site =>
      {

      });

      If you’re still having problems, feel free to post the code that you expect to compile. Note that WordPress likes to eat < >, so make sure you escape them: &lt;

      Cheers ~
      Keith

  3. Alex Says:

    Hi Keith,

    thanks for the quick reply, and thanks for this great article!

    My method is like the SomeFunction() mentioned above. I thought I should be possible to invoke it like this:

    string result = site.SelectAsSystem(SomeFunction);

    The compiler error was:
    The type arguments for method ‘MyProject.SPSecurityExtensions.SelectAsSystem<T>(Microsoft.SharePoint.SPSite, System.Func<Microsoft.SharePoint.SPSite,T>)’ cannot be inferred from the usage. Try specifying the type arguments explicitly.”

    So I thought I should type cast my method as mentioned in my first post, and it seems to work (need to test it further)

    Thanks again!
    Alex

  4. Keith Dahlby Says:

    Glad to help! I’ve never noticed that specific behavior, but you are indeed correct. Rather than cast the delegate, a slightly cleaner option is to specify the type on the method call instead:

    string result = site.SelectAsSystem<string>(SomeFunction);

    As to why the compiler can’t infer the type of methods, the short answer is overloads. Note the wording on the error if you try to set var f = SomeFunction: “Cannot assign method group to an implicitly-typed local variable.” Overloads don’t apply here since our Func can only accept an SPSite, but in general the problem is hard enough that the compiler doesn’t even try. This also explains why lambdas and anonymous methods can be passed in without specifying the type – they can’t have overloads!

    Hope this helps ~
    Keith

  5. The MOSS-pit : A catch using SPSecurity.RunWithElevatedPrivileges Says:

    [...] the functionality in my SharePoint.Common library) and recommend reading his article – see http://solutionizing.net/2009/01/06/elegant-spsite-elevation/Keep up the good work [...]

  6. Alex Says:

    Keith,

    thanks again, I’ve never been using the Extensions, but this is an example of the real they provide!

    I’m now using this method, as it has multiple advantages:
    it’s elegant, and it is easy to pick up the actual user in the priveleged code (e.g. from the thread)

    A tip: if you need to do something with elevated priveleges and pass multiple parameters, a simple workaround is to create a helper class:

    class Helper
    {
    private params …

    public Helper(params…)
    {
    store params in helper class

    }

    public DoIt(SPWeb web)
    {

    }
    }

    and in the main code:

    Helper h = new Helper(params…)
    web.RunAsSystem(h.DoIt);

    Cheers,
    Alex

  7. djeeg Says:

    Im not totally convinced that using SPUserToken is “better” than RunWithElevatedPrivileges.
    As an example try this:

    SPSecurity.RunWithElevatedPrivileges(delegate() {
    using (SPSite site = new SPSite(“http://sharepoint”)) {
    using (SPWeb web = site.OpenWeb()) {
    SPList list = web.Lists["Test List"];
    SPListItem item = list.Items.Add();
    item["Title"] = “Title 1″;
    item.Update();
    }
    }
    });

    SPUserToken token;
    using (SPSite csite = new SPSite(“http://sharepoint”)) {
    token = csite.SystemAccount.UserToken;
    }
    using (SPSite site = new SPSite(“http://sharepoint”, token)) {
    using (SPWeb web = site.OpenWeb()) {
    SPList list = web.Lists["Test List"];
    SPListItem item = list.Items.Add();
    item["Title"] = “Title 2″;
    item.Update();
    }
    }

    Then look at the item properties. See how the one created with RunWithElevatedPrivileges has the context user as the creator. But the one created with the token has the system account as the creator. As long as you know there are differences between the two methods.

    • plissskin Says:

      djeeg, I am using your method of the delegate RunWithElevatedPrivileges in a WebPart that does UNC file access to build a explorer type tree in the web part. It deploys and works fine on our test WSS 3.0 server farm, but when I deploy to our production farm, it seems to deploy and install normally. But when you try to add the web part to any page you get the import error message (“Cannot import web part”). Any ideas on why? I have beat myself to death on the usual suspects, Version number, public key, is the dll in the GAC, etc. It seems to be something about the RunWithElevatedPrivileges.

      • Keith Dahlby Says:

        The ULS logs (12\LOGS by default) should show you exception details to help pinpoint the error. One “usual suspect” you didn’t list would be a SafeControl entry. Other than that, my first guess would be that your production application pool identity doesn’t have sufficient permissions on the file share.

  8. Keith Dahlby Says:

    djeeg ~

    Judging by the lack of web.AllowUnsafeUpdates, I’m guessing you tried that from a console application rather than in a web context. If the current process doesn’t use impersonation (console app, stsadm, timer job), RWEP does nothing, which is why you would see the context user as the creator.

    To see the real difference between the two methods, stick your code in a webpart and set title to Environment.Username. The RWEP block will show the app pool username, the other will show the current user, and both items will be created by System Account.

    Cheers ~
    Keith

  9. Five Important Tests Before You Go Live | SharePoint Magazine Says:

    [...] Practices for Elevated Privilege in SharePoint and Elevated Privilege with SPSite by Daniel Larson Elegant SPSite Elevation by Keith [...]

  10. chinu Says:

    I am still getting access denied error while trying to change site quota impersonating systemaccount. When I login as system account the code works. any idea why? I have already setup oweb.allowunsafeupdates to true.

  11. Daniel Says:

    Thank you! GetSystemToken saved my ass today!

  12. Christopher Deweese Says:

    Keith,

    Over 2 years later this post is still helpful :) Great samples and technique..just used it to fix a nasty problem that was giving us lots of headaches. Hope to see you at Day of .NET this year!

  13. Scott Brickey Says:

    Reading the MSDN articles, it seems that they’re recommending use of:
    static property SPUserToken.SystemAccount
    instead of looping through SPSite.SystemAccount.UserToken


Comments are closed.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: