Updated on 11/15/05 with links to reference articles on impersonation and code access security
Published: June 12, 2004
Today's topic requires advanced coding knowledge. I will try my best to layout the fundamental pieces, but to prevent this post from becoming too large, you may be required to read external references.
There are some interesting limitations placed on the SharePoint OM. Those limitations can really make some operations very hard if not impossible to complete.
In some cases, you can revert to self (thus assuming the identity of the AppPool) or otherwise impersonate a user identity with higher permissions than the current user to allow certain
OM methods to execute. On top of that, you may have used the
SPSite.CatchAccessDeniedException property to prevent those annoying authentication dialogs from appearing. However, there are times, neither technique will suffice.
Here's an example:
foreach (SPRole r in this.CurrentWeb.CurrentUser.Roles) {
writer.WriteLine (Utility.ParagraphEnclose("Role: " + r.Description));
}
For an admin, this works great. For any other user, this absolutely fails. Even if you were to wrap this call within the context of an impersonated user, it would still fail.
Why?
The SharePoint object model, when running under the context of an IIS request, will always validate its actions against the original context of the request. Therefore, reverting to self or impersonating an admin account is absolutely worthless.
Is this a bug or bad design?
It's both. However, that's another topic altogether.
Are there any solutions?
Yes... and this where you need to put your coding hat on.
The limiting factor is the fact the OM always looks at the original identity of the http context. If you can step outside of that context, the OM will operate in a "normal" console-like fashion. It will be forced to use identity of the thread, rather than attempting to fallback to the identity of the http context.
How can this be accomplished?
It's quite simple, but rather complex at the same time.
- Revert to self or impersonate
- Create an AppDomain (I'll
refer to this as the Secondary AppDomain)
- Do the work in the Secondary AppDomain.
- Marshall back the necessary results
- Unload the AppDomain
- Resume orginal identity
Please note the following terminology will be used to describe where an action occurs:
- Primary AppDomain: the original AppDomain where your WebPart is running. This AppDomain was created by Asp.Net.
- Secondary AppDomain: the AppDomain created by your WebPart code.
If you're not familiar with the concept of AppDomains, I strongly suggest that you visit MSDN for more information. There are a large number of articles which describe how AppDomains can be used. Here's a small set of useful links:
.Net Framework Class Library: AppDomain Class
Suzanne
Cook's .NET CLR Loader Notes: Unloading an Assembly
cbrumme's
WebLog: AppDomains ("application domains")
As you can see, AppDomains offer a great way of isolating code. In our case, this isolation ensures we effectively "lose" the http context whenever working in the secondary AppDomain. However, getting data from one AppDomain to another requires some work as it does not come for free. The AppDomain class offers functionality which allows you to execute code in a secondary AppDomain.
This method is rather clunky as you must first set data, execute, and finally
retrieve data (reference
AppDomain.DoCallBack Method for more information). I prefer to use a different technique, which is discussed in just a bit.
The key thing to remember when working with AppDomains and, more importantly, marshalling data between AppDomains is the fact you wish isolate which assemblies are loaded in which AppDomain. You can pass any type of data between AppDomains; however, if your secondary AppDomain attempts to pass a complex object, the CLR will attempt to load the assembly which contains that object in the primary AppDomain. For this discussion, this factoid may not absolutely critical, but it definitely something to remember given that loading an assembly will lock the given assembly.
My favorite technique for using AppDomains requires a little more overhead, but it is amazingly flexible. The technique is fondly called the "interface loader" technique. It works on the premise that method execution is controlled via an interface. This makes your code much easy to maintain and use once you've setup the necessary framework. Here's a quick summary of what is needed:
- Define an interface class
- Define a remote loader class which implements the interface
- Define a factory class which
- Code in primary AppDomain executes methods via interface.
To help all of this make sense, some code sample
is needed.
I'll quickly outline each section so that you can see how all of this fits together. Please note that in order to load assemblies in the secondary AppDomain, a higher set of permissions is required both from the code access security side and the user identity. If you want a complete list of permissions, you'll have to dig into the msdn documentation.
Update
11/15/2005
For more information on code access security, please check out the
following posts:
For this example, I would simply suggest I would simply suggest a) grant your
assembly FullTrust by installing the assembly into the GAC (yes, this is an
anomaly for me) and b) impersonate an admin as noted in the comments.
To help make this code as simple as possible, all calls to revert to self/impersonate have been eliminated. Additionally, please note this sample is in no way optimized. Before implementing this technique, please be aware of performance and memory consumption implications. The purpose of the sample code is is to demonstrate how to implement the loader interface technique. You should definitely tailor the code to account for shadow copying and other advanced AppDomain properties (especially if you intend to install your assembly in the bin folder).
The loader interface technique is without a doubt one of the best tools in a coder's arsenal. I've used the technique for the past two and half years on a wide variety of projects... and, as you can see, it can be readily used to make the SharePoint OM ignore an http context to help make your WebPart code run under all user identities.
-Maurice
Step 1 - Define an interface class
public interface IRemoteMethods : IDisposable {
string ListRoles (Guid siteID, Guid webID, Guid userID);
} // End of IRemoteMethods interface
Step 2 - Define a remote loader class which implements the interface
public class RemoteMethods : MarshalByRefObject, IRemoteMethods {
string ListRoles (Guid siteID, Guid webID, Guid userID) {
string userRoles = null;
using (SPSite site = new SPSite(siteID)) {
using (SPWeb web = site.OpenWeb (webID)) {
foreach (SPRole r in web.Users.GetByID(userID).Roles) {
userRoles += Utility.ParagraphEnclose("Role: " + r.Description);
}
}
}
return userRoles;
} // End of ListRoles
public void Dispose() {
// Add dispose code...
} // End of Dispose
} // End of RemoteMethods class
Step 3 - Define a factory class
public class RemoteInterfaceFactory : IDisposable {
private AppDomain appDomain = null;
private IRemoteMethods remoteInterface = null;
public IRemoteMethods Loader {
get {
return remoteInterface;
}
} // End of property Loader
public RemoteInterfaceFactory () {
this.appDomain = AppDomain.CreateDomain (SecondaryAppDomain, AppDomain.CurrentDomain.Evidence);
// Create an instance of the RemoteMethods class in the secondary domain
// and get an interface to that instance. This ensures you can use the
// instance but are not loading the instance into the primary AppDomain.
remoteInterface = (IRemoteMethods) appDomain.CreateInstanceAndUnwrap (Assembly.GetExecutingAssembly().GetName().FullName, typeof(RemoteMethods).FullName);
} // End of RemoteInterfaceFactory
public void Dispose() {
if (this.remoteInterface != null) {
this.remoteInterface.Dispose ();
this.remoteInterface = null;
}
if (this.appDomain != null) {
AppDomain.Unload (appDomain);
this.appDomain = null;
}
} // End of Dispose
} // End of RemoteInterfaceFactory class
Step 4 - Code in primary AppDomain executes methods via interface
string listOfCurrentUserRoles = null;
// Impersonate or call revert to self
(this is left to the reader to implement)
using (RemoteInterfaceFactory remoteFactory = new RemoteInterfaceFactory ()) {
listOfCurrentUserRoles = remoteFactory.Loader.ListRoles (siteID, webID, userID);
}
// resume original identity
writer.WriteLine (listOfCurrentUserRoles);