Tuesday, March 28, 2006

Who Am I? Demystifying SharePoint Impersonation.

The SharePoint security model makes it easy to programmatically execute code within the current user context. Just write and deploy your web part and it runs in the security context of the logged in user. There are even built-in functions that take advantage of the user’s security context – such as GetSubwebsForCurrentUser() – without requiring any extra coding on our part. Simple yet effective.

But what about those times when you need to execute code with permissions greater than that of the current user (like instantiating a site collection or enumerating list permissions)? Well, that’s not quite so easy, but it’s not that hard, either – Lois and Clark have an excellent post on various methods for altering security context. There’s also Todd Bleeker’s method and Maurice Prather’s approach. And just to cook your noodle a bit more, what about those times when you not only need to run in an administrative context but you also need to check permissions for the current user? So much for simple.

Thankfully, there is a relatively straightforward way to achieve both objectives. By combining a bit of RevertToSelf() with AppDomains and SPRoleCollections, we can run securely in an administrative context without hard-coding account details, force SharePoint to use the credentials we give it, and verify that users have access to the content being displayed. In fact, this is the only way I’ve found to enforce impersonation in all instances.

I must admit to not being the world’s greatest programmer, so I spent quite a bit of time flipping between each code snippet trying to piece together a solution (code wizards like Simser and Tielens can do this stuff in their sleep but I seem to be lacking an equivalent concentration of grey matter). To save you from the same headaches, here’s the code in full with a complete description afterwards.

using System;
using System.Data;
using System.Text;
using System.Drawing;
using System.Collections;
using System.Configuration;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Security;
using System.Security.Policy;
using System.Security.Principal;
using System.Security.Permissions;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebPartPages;
using Microsoft.SharePoint.WebControls;

namespace MyNameSpace
{
public class MyClass : System.Web.UI.Page
{
[DllImport("advapi32.dll")]
static extern bool RevertToSelf();

private void Page_Load(object sender, System.EventArgs e)
{
WindowsIdentity objOriginalUser = WindowsIdentity.GetCurrent();
RevertToSelf();
WindowsIdentity.GetCurrent().Impersonate();

try
{
AppDomainSetup objAppDomainSetup = AppDomain.CurrentDomain.SetupInformation;
Evidence objEvidence = AppDomain.CurrentDomain.Evidence;
AppDomain objMyAppDomain = AppDomain.CreateDomain("MyAppDomain", objEvidence, objAppDomainSetup);

SPSite site = new SPSite(<portal URL>);
site.CatchAccessDeniedException = false;
SPWeb web = site.AllWebs[<site URL>];
SPPermissionCollection perms = web.Permissions;
SPUserCollection users = web.Users;

foreach (SPUser user in users)
{
if (objOriginalUser.Name.ToLower() == user.LoginName.ToLower())
{
SPRoleCollection roles = user.Roles;
SPListCollection lists = web.Lists;

foreach (SPRole role in roles)
{
foreach (SPPermission perm in perms)
{
if (role.ID == perm.Member.ID)
{
if
(role.PermissionMask.ToString().IndexOf("ViewPages") != -1 role.PermissionMask.ToString().IndexOf("FullMask") != -1)
{
// TODO: Insert your code here.
}
}
}
}
}
}
}
catch(System.UnauthorizedAccessException ex)
{
Response.Write(ex.Message.ToString());
}
}
AppDomain.Unload(objMyAppDomain);
}
}


Before we go any further, allow me to point out that the code sample is probably causing real .NET developers to pull their hair out – for one, I don’t know all their code optimization techniques, and for another, I’m trying to keep it simple so this post doesn’t run off the end of the page, spill over the keyboard, and end up in the reader’s lap. So have some mercy and don’t flame me too badly but do post any tips that will help make the code better. Also, note that throughout this post I’ve tried to use the same variable names and syntax as the original articles to avoid confusion.

Now on to business. In the above example, we are instantiating a new SPWeb object in the Administrator context and checking the permissions for the actual logged-in user for the target site. The first step is to make sure we have the proper references so be sure to add the proper DLL’s to your project and add all the required ‘using’ statements (you may need more or less depending upon what your application does).

using System;
using System.Data;
using System.Text;
using System.Drawing;
using System.Collections;
using System.Configuration;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Security;
using System.Security.Policy;
using System.Security.Principal;
using System.Security.Permissions;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebPartPages;
using Microsoft.SharePoint.WebControls;

Next, we need to import the ‘advapi32.dll’ in order to facilitate impersonation of user accounts.

[DllImport("advapi32.dll")]
static extern bool RevertToSelf();

Once we have the proper resources, we can impersonate the administrative user account. The RevertToSelf() function returns the user credentials of the account under which the SharePoint application pool runs in IIS (MSSharePointPortalAppPool in a default installation). If you are running your portal under a different context, now would be a good time to change it to a local admin account that is also a SharePoint administrator. The biggest advantage of this method is that it does not required any sensitive information (such as passwords) to be stored in clear text; the credentials are specified in the IIS settings for the virtual server.

WindowsIdentity objOriginalUser = WindowsIdentity.GetCurrent();
RevertToSelf();
WindowsIdentity.GetCurrent().Impersonate();

The first line gets the current user account for later use. The second calls the Application Pool credentials and the third sets the impersonation context to that account (omitting it will generate an Access Denied error).

Once we have the proper credentials in place, it’s necessary to create a unique application domain to run our code in. SharePoint has a nasty quirk wherein it insists on using the currently logged in user credentials no matter how many times you tell it otherwise. The fix for this is to create an application domain wrapper for the subsequent code, in effect fooling the SharePoint object model into thinking your web application is actually a console application (see Maurice’s post for a full explanation).

AppDomainSetup objAppDomainSetup = AppDomain.CurrentDomain.SetupInformation;
Evidence objEvidence = AppDomain.CurrentDomain.Evidence;
AppDomain objMyAppDomain = AppDomain.CreateDomain("MyAppDomain", objEvidence, objAppDomainSetup);

Now that we have the proper credentials and domain in place we can move on to the actual SharePoint code. For this very basic example, we first instantiate a new site based on the root portal, then create an SPWeb object for a site so we can get its user and permission collections. Note the ‘CatchAccessDeniedException() method – this prevents authentication dialog boxes from appearing if the login somehow fails.

SPSite site = new SPSite(<portal URL>);
site.CatchAccessDeniedException = false;
SPWeb web = site.AllWebs[<site URL>];
SPPermissionCollection perms = web.Permissions;
SPUserCollection users = web.Users;

Earlier we captured the original user credentials before impersonating the admin account. We can now use that information to compare against the list of site users to see if the currently logged-in user has permissions to access the site. First, we’ll check to see that the user names match:

if (objOriginalUser.Name.ToLower() == user.LoginName.ToLower())

Then we’ll get the security role collection for the site:

SPRoleCollection roles = user.Roles;

Then we loop through the security roles and permission collections to see if the role ID for the current user matches the Member ID in the permissions table for the site. If it does, then we check to see if the user has either the ViewPages right or, if the user is an administrator, the FullMask right (rights are cumulative for Web Designers, Contributors, Readers and all other roles except Administrators, who simply have the ‘FullMask’ permission).

foreach (SPRole role in roles)
{
foreach (SPPermission perm in perms)
{
if (role.ID == perm.Member.ID)
{
if
(role.PermissionMask.ToString().IndexOf("ViewPages") != -1 role.PermissionMask.ToString().IndexOf("FullMask") != -1)


Assuming the user credentials are accepted, you may now execute whatever code you desire with confidence that the user has sufficient permissions to view the specified content. Once the code has finished executing it’s a good idea to destroy the application domain you created and, if you wish, revert back to the original user context.

AppDomain.Unload(objMyAppDomain);
objOriginalUser.Impersonate();

Of all the methods that are available for running code under an administrative context in SharePoint, this combination of impersonation and application domains is the only one that I’ve found works in every situation I’ve tried.