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.

Advertisement