Implementing and Application Domain (AppDomain) and impersonation

on August 22, 2006

During the development I faced a major problem. To display the items the user has the right for, I need to look in every list and use methods that requires admin rights.

For example, to loop through a collection of List, you need to have access to all Lists. Same thing if you one to use “Count” on a collection.
To fix this problem, I used “Impersonation”. According to Microsoft, the Best Practice for Impersonation is to use the application pool account. In many cases that is enough. If you start coding a web part try this before going into an Application Domain.
I did many tests because there are many ways to impersonate. The best I’ve found is this one. I only slightly modified the class to optimize some methods.

Find my Impersonation Class in the following weblog article:

http://www.e-soft.ca/blogs/index.php?title=impersonation_class&more=1&c=1&tb=1&pb=1

But in my case it wasn’t enough. Indeed, the code security was still pushing access denied exception.
I found out that it double check the security. When you impersonate, you take the Windows Identity, but the Original HTTP User will always stay “alive”. Than the code fails because it checks both places (Windows Identity AND HTTP Users) to be sure the code is not being hijacked.
Fortunately, there is a work around for this. Although neither recommended nor supported by Microsoft, it is quite the only solution I found at the moment. The use of a custom Application Domain.
I was first directed to this blog to get the “How-to”:
http://www.bluedoglimited.com/SharePointThoughts/ViewPost.aspx?ID=7
This blog article is very good and recommend all over the world ;-) but has some minor errors in the code. I recommend that you read the whole thing to get a better understanding so I do not write the same text here.
To know a little about appdomain at MSDN select his url: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemappdomainclasstopic.asp

Again, BlueDogLimited did a great job here. As they say, the code is not optimized; I kiss their toes… ;-) Because if wouldn’t have been redirected to their site, I would never had been able to finish this web part. Based on that, take my explanation as a research that began with their example.

One problem with their code is that you need to place the assemblies in the GAC to make it works. This is something I do not perform for 2 reasons: Even if strongly named (Required to be in the GAC) It opens a breach for hijackers. Next, it adds complicity when you are debugging, because it is also not recommender to output the assemblies directly to the GAC from Visual Studio.

With the help of a great guy at Microsoft, I modified the code so the Application domain look in the Bin directory for ALL the assemblies required for the web part. Yes, it will also load the .dll means,
Their code to load the assembly is this one:

Code:

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

Here is my code, providing a way to retrieve the assamblies from there initial location:

Code:

public RemoteInterfaceFactory (string SecondaryAppDomain)
{
try
{
//I modified the constructor so I receive a unique SecondaryAppDomain Name. This way I am sure not to share stuff with another web part.
//This Code make it possible to NOT put the dll in the GAC. Easier to debug.
this.appDomain = AppDomain.CreateDomain(SecondaryAppDomain, AppDomain.CurrentDomain.Evidence,System.AppDomain.CurrentDomain.BaseDirectory,System.AppDomain.CurrentDomain.RelativeSearchPath,false);
string an = System.Reflection.Assembly.GetExecutingAssembly().Location;
//an = "c:inetpubwwwrootbinMyDll.dll";
remoteInterface = (IRemoteMethods)appDomain.CreateInstanceFromAndUnwrap(an, "webpartlibrary.RemoteMethods"); //remplacer Namespace.RemoteMethods par le nom complet de votre propre classe
//remoteInterface = (IRemoteMethods) appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().GetName().FullName, typeof(RemoteMethods).FullName);
// 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 AppD
}
catch(Exception ex)
{
Debug.WriteLine(ex.Message+" "+ex.StackTrace);
throw(new ApplicationException(ex.Message, ex));
}
} // End of RemoteInterfaceFactory</pre>

If you already know about the implementation of an AppDomain, that will definitely help you. But if you don’t know… here is excerpts of each methods required to make it works.
My web part use Asynchronous calls, cache, impersonation and AppDomain. All of this has direct and indirect impact on the code and the execution of the code. I will not provide explanation on Asynchronous calls and Cache to reduce the amount of text. Maybe on another article.
As for any web part, RenderWebPart is called. Note that what you see about Async is not required if you intent to remove or not use the Asyncrnous calls. You may leave doWebPart() followed by the output.Write(buffer.ToString()); and it would do the work.

Code:

protected override void RenderWebPart(HtmlTextWriter output)
{
if(!USE_ASYNCRONOUS_CALL)
doWebPart();//This is commented by the web part is now using asynchroneous call.
//buffer is filled by doWebPart()
//If no errors
output.Write(buffer.ToString());
}


Here DoWebPart() implement the Cache functionality. _cacheTimeOut is an integer from a custom web part property. If you intent to skip the Cache feature, you get bypass this method and directly call GetMyWebPart(); .

Code:

private void doWebPart()
{
try
{
//Write to cache content ONLY if Cache Emtpy AND CacheTimeout is Greather than 10 seconds. Lower than this, it may cause emtpy content.
if ((this.PartCacheRead(Storage.Personal, this.EffectiveTitle) == null)&amp;(_cacheTimeOut>10))
{
/* if webpart html is not in cache, add it to cache
* I use this.EffectiveTitle as cache name so it is dynamic. can be cut and pasted n any webpart code.*/
this.PartCacheWrite(Storage.Personal, this.EffectiveTitle, GetMyWebPart(), new TimeSpan(0, 0, _cacheTimeOut));
buffer.Append(Convert.ToString(this.PartCacheRead(Storage.Personal, this.EffectiveTitle)));
}
else
{
//Retrieve cached output and write it to the StringBuilder "buffer". Is then used by RenderWebPart()
//Grab cache content ONLY if CacheTimeout is Greather than 10 seconds. Lower than this, it may cause emtpy content.
string appendThis = GetMyWebPart();
if(appendThis!=null)
buffer.Append(appendThis);
}
}
//+++++++ This is the last catch of the application
catch(Exception cx)
{
Debug.WriteLine(cx.Message +" "+cx.StackTrace);
string exceptionMessage="There has been an error while creating "+this.Title+" web part.";
buffer.Append(exceptionMessage ++cx.Message+" "+cx.StackTrace);
}
}

Here begin the process for the application domain.
With “string SecondaryAppDomain = this.EffectiveTitle.Replace(” “,"")+"AppDomain";” I ensure that I have a unique string to create the name of the ApplicationDomain. I did this modification because I thought that if I had many web part using that kind of specific application domain, it could lead to some mixing has it can arise when closing SPWeb objects.

Note the argument I for BuildwebPart = remoteFactory.Loader.BuildWebPartHTML(………). When the AppDomain is loaded, the is absolutely no connection between the to Application Domain. So if you need to access vars that are created arlier than here, you must pass them through this methods and all along the process. Another important warning… You cannot pass any complex type. Even an array may fail. You’ll receive a strange error message (Sorry, can’t remember wich one). Even the Sharepoint Context object cannot go through (Instead you can use the URL to open SPWeb or/and SPSite.)

Code:

#region GetMyWebPart()
/// I created this method because of the cache process in doWebPart()
/// And of course for the impersonation/AppDomain process.
private string GetMyWebPart()
{
string BuildwebPart = string.Empty;
try
{
//Step 4 of ApplicationDomain Implemention - Code in Primary AppDomain executes methods via interface.
string SecondaryAppDomain = this.EffectiveTitle.Replace(" ","")+"AppDomain";
using (RemoteInterfaceFactory remoteFactory = new RemoteInterfaceFactory (SecondaryAppDomain))
{
//Here is the place where i'll need to add the parameters.
// Because the AppDomain isolate everything.
BuildwebPart =
remoteFactory.Loader.BuildWebPartHTML( //SKIP_WSS_SECTION,
_ServerUrl,
this._listFields,
this._listFieldsPubDate,
this._includeLists,
//this._includeSites,
_itemsToDisplay,
_itemIconUrl,
_newWindow,
_showListLink,
//_showWSSNameInFrontOfListLink,
_showSPSNameInFrontOfListLink,
_showDate,
//_SKIPTEMPTYTITLE,
this.EffectiveTitle);
}
}
catch(Exception ex)
{
Debug.WriteLine(ex.Message+" "+ex.StackTrace);
throw(new ApplicationException(ex.Message, ex));
}
return BuildwebPart;
}
#endregion


Than you need the interface. Again you identify your own set of variables. Of course it must match with the type and number of the original set.:

Code:

public interface IRemoteMethods : IDisposable
{
string BuildWebPartHTML(//bool SKIP_WSS_SECTION,
string ContextURL,
string slistFieldsHashTable,
string sincludeListsHashTable,
string sincludeListsPubDateHashTable,
//string sincludeSitesHashTable,
int ItemsToDisplay,
string ItemIconUrl,
bool NewWindow,
bool ShowListLink,
//bool ShowWSSNameInFrontOfListLink,
bool ShowSPSNameInFrontOfListLink,
bool ShowDate,
//bool SkipEmptyTitle,
string EffectiveTitle);
} // End of IRemoteMethods interface


Now, you have the RemoteMethods Class… :
I leave the vars init in the example, so you can see the relation between the passed vars and the following.

Code:

#region class RemoteMethods : MarshalByRefObject, IRemoteMethods
/// Second Part of implement ApplicationDomain Technique for proper impersonation.
/// BuildWebPartHTML is what is usually called from my doWebPart Method (That is for Cache purpose)
public class RemoteMethods : MarshalByRefObject, IRemoteMethods
{
#region Vars
const string _defaultDateField = "Modified";
System.Text.StringBuilder output = new System.Text.StringBuilder();
System.Collections.ArrayList items;
System.Collections.Hashtable listBelongTo;//Provide a HashTable to store List ID with value of SPS or WSS to know on wich system this list belgong
int numList = 0;
int selNumList = 0;
SPSHelpers spsh;
string EffectiveTitle;
//Here we init the var of the field we will use for all of our date work (For display AND for.
string dateField = _defaultDateField;//Default To modified because it is always there.
//bool SKIP_WSS_SECTION;
string ContextURL;
ImpersonationUsingRevertToAppPool im;
System.Collections.Hashtable listFieldsHashTable;
System.Collections.Hashtable listFieldsPubDateHashTable;
System.Collections.Hashtable includeListsHashTable;
//System.Collections.Hashtable includeSitesHashTable;
string listFields;
string listFieldsPubDate;
string includeLists;
//string includeSites;
int ItemsToDisplay;
string ItemIconUrl;
bool NewWindow;
bool ShowListLink;
//bool ShowWSSNameInFrontOfListLink;
bool ShowSPSNameInFrontOfListLink;
bool ShowDate;
//bool SkipEmptyTitle;
#endregion

The build web part method is what was initially called when there was no Application Domain implemented. This is where the webpart is actually created,

Code:

#region BuildWebPartHTML(ManyManyMany)
/// In the code provided at the web site, the method is not declared public.
/// But it needs to be. If not, you get an error.
/// Do not forget not to add complex type to the parameters. It would lead to exceptions.
/// Only simple types are allowed.
public string BuildWebPartHTML( //bool SKIP_WSS_SECTION,
string ContextURL,
string slistFieldsHashTable,
string slistFieldsPubDateHashTable,
string sincludeListsHashTable,
//string sincludeSitesHashTable,
int ItemsToDisplay,
string ItemIconUrl,
bool NewWindow,
bool ShowListLink,
//bool ShowWSSNameInFrontOfListLink,
bool ShowSPSNameInFrontOfListLink,
bool ShowDate,
//bool SkipEmptyTitle,
string EffectiveTitle)
{
//this.SKIP_WSS_SECTION = SKIP_WSS_SECTION;
this.ContextURL = ContextURL;
this.im = new ImpersonationUsingRevertToAppPool();
this.listFields = slistFieldsHashTable;
this.listFieldsPubDate = slistFieldsPubDateHashTable;
this.includeLists = sincludeListsHashTable;
//this.includeSites = sincludeSitesHashTable;
this.ItemsToDisplay = ItemsToDisplay;
if (this.ItemsToDisplay &lt;9)
this.ItemsToDisplay=9;
this.ItemIconUrl = ItemIconUrl;
this.NewWindow = NewWindow;
this.ShowListLink = ShowListLink;
//this.ShowWSSNameInFrontOfListLink = ShowWSSNameInFrontOfListLink;
this.ShowSPSNameInFrontOfListLink = ShowSPSNameInFrontOfListLink ;
this.ShowDate = ShowDate;
//this.SkipEmptyTitle = SkipEmptyTitle;
this.EffectiveTitle = EffectiveTitle;
try
{
//HERE YOU PUT THE CODE FOR THE CREATION OF YOUR WEBPART
}
catch(Exception ex)
{
Debug.WriteLine(ex.Message+" "+ex.StackTrace);
throw(new ApplicationException(ex.Message, ex));
}
return output.ToString();
}
#endregion
public void Dispose()
{
// Add dispose code...
} // End of Dispose
} // End of RemoteMethods class
#endregion
#region class RemoteInterfaceFactory : IDisposable
/// This Class is for ApplicationDomain/Impsersonation purpose.
/// It is Step 3 of information at: http://www.bluedoglimited.com/SharePointThoughts/ViewPost.aspx?ID=7
public class RemoteInterfaceFactory : IDisposable
{
private AppDomain appDomain = null;
private IRemoteMethods remoteInterface = null;
public IRemoteMethods Loader
{
get
{
return remoteInterface;
}
} // End of property Loader
/// Class Constructor
public RemoteInterfaceFactory (string SecondaryAppDomain)
{
try
{
//I modified the constructor so I receive a unique SecondaryAppDomain Name. This way I am sure not to share stuff with another web part.
//This Code make it possible to NOT put the dll in the GAC. Easier to debug.
this.appDomain = AppDomain.CreateDomain(SecondaryAppDomain, AppDomain.CurrentDomain.Evidence,System.AppDomain.CurrentDomain.BaseDirectory,System.AppDomain.CurrentDomain.RelativeSearchPath,false);
string an = System.Reflection.Assembly.GetExecutingAssembly().Location;
//an = “c:inetpubwwwrootbinMyDll.dll";
remoteInterface = (IRemoteMethods)appDomain.CreateInstanceFromAndUnwrap(an, “webpartlibrary.RemoteMethods"); //replace Namespace.RemoteMethods by the full name of your own class. NOTE THAT THIS COULD BE AUTOMATED USING Reflection BUT I DID’NT TOOK THE TIME TO DO IT. ALSO, FOR OUR COMPANY’ IT WILL NEVER CHANGE… THAT IT ALSO WHY IT IS NOT A Var.
//remoteInterface = (IRemoteMethods) appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().GetName().FullName, typeof(RemoteMethods).FullName);
// 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 AppD
}
catch(Exception ex)
{
Debug.WriteLine(ex.Message+” “+ex.StackTrace);
throw(new ApplicationException(ex.Message, ex));
}
} // End of RemoteInterfaceFactory
public void Dispose()
{
try
{
if (this.remoteInterface != null)
{
this.remoteInterface.Dispose ();
this.remoteInterface = null;
}
if (this.appDomain != null)
{
AppDomain.Unload (appDomain);
this.appDomain = null;
}
}
catch(Exception ex)
{
Debug.WriteLine(ex.Message+” “+ex.StackTrace);
throw(new ApplicationException(ex.Message, ex));
}
} // End of Dispose
} // End of RemoteInterfaceFactory class
#endregion

Feel free to leave comments. I’ll wrote more articles about some particularities, like the Cache, the async calls and so on.

0 comments:

ShareThis