September 2004 - Posts
Many people have stumbled across the IIS Double-Hop issue when trying to access Active Directory information on an ASP.NET page.
In short - when we access an ASP.NET page, IIS can impersonate our user credentials for code execution (if set in the web.config). On Windows networks, impersonated credentials can't be used to access network resources - this prevents a web-server from maliciously or accidently using the credentials of a user who innocently logged on to it to access protected network resources.
The problem is that when we try to access the Active Directory for legitimate purposes (to display the current user's details, for instance) while impersonating the user (as SPS does by default) we can't pass the credentials on and our AD query uses the understandbly-limited Anonymous Logon account.
The suggested workaround is to supply teh DirectoryServices request with a username and password so as not to use the impersonated credentials - but this, of course, opens up a whole set of problems relating to security and maintainability.
This workaround will work only if the IIS worker process is running under a domain account.
Under IIS 6 - this is the user specified in the Identity tab of the Application Pool.
Under IIS 5 - this is the user specified in the Identity tab of the COM+ application that is generated for the IIS application.
Since our problem arises from having our code run in an impersonation context, the solution is simply to undo the impersonation so that the executing identity isn't the connecting user, but the user who started the process. Since we don't have any convenient .NET WindowsImpersonationContext object we can Undo(), we'll simply have to go deeper into Win32 code:
private static extern int RevertToSelf();
This very simple function (no parameters - no marshalling! Yipee!) will cancel the current impersonation session and return the actual running user - our IIS WP identity.
It would be prudent to re-impersonate the connected user after our code runs:
WindowsIdentity connectedUser = WindowsIdentity.GetCurrent();
// DirectoryServices code here.
If this revert/reimpersonate code is something we'll be running a lot, it would be nice to wrap this in a nice disposable object so we can wrap our code in a using() block:
using (new UnImpersonator())
// Code running as the Worker Process identity.
This is similar to the ImpersonationContext class I described here - the constructor will save the current Identity in a member and call Revert, and the Dispose() will re-impersonate the user.
In the first part of the post we went over binding an SPListItemCollection to a DataGrid for quick'n'dirty binding of Sharepoint list data. The problem with this approach is that extensibility is a problem. To paraphrase the specific request I got: "Remember that webpart where you put the List items in a grid? Now I want you to have the grid show the items from two other sites too".
In our first approach, we would be somewhat screwed - the SPListItemCollection represents items in a single list, and I can't merge several of those to one object. I would have to start writing code to take the data out of my SPListItems and putting them into DataRows and do a lot of dirty work - but luckily Sharepoint does that dirty work for us already:
DataSet allItems = new DataSet();
This code effortlessly puts a DataTable (with the same name as our List) into a DataSet - no fuss, no mess.
If I now have several lists with the same name and structure but in different sites, I can run the Merge for each and have them all fit in one nice table, and then bind to them in the usual fashion:
<%# DataBinder.Eval (Container.DataItem, "MyCustomProperty") %>
In my case, I wanted the grid to show the items for the selected site's list and for all its subsites, so a simple recursive function got me a DataTable with all items included:
private DataSet getAllItems (SPWeb web, string listName)
DataSet items = new DataSet();
foreach (SPWeb subweb in web.Webs)
Note: For brevity's sake I've omitted try/catch blocks, but don't forget that most SPS collections don't have a Contains function, and collection accesses like web.Lists must be protected with try/catch.
A further thing I can do with this approach is add more data - like a computed column - into the table:
private DataSet getItemsWithUrl (SPWeb web, string listName)
DataSet items = new DataSet();
SPList list = web.Lists[listName];
string displayUrl = web.Url + "/" + list.Forms[PAGETYPE.PAGE_DISPFORM].Url + "?ID=";
DataTable itemTable = list.Items.GetDataTable();
DataColumn UrlColumn = new DataColumn ("Url", typeof(string), "'" + displayUrl + "' + ID");
This variant on the previous function gets the list's default Display Item Form from the Forms collection and builds the URL. The computed column we added concats this base URL to the ID of the item, and presto - we have a link to display the ListItem:
<asp:HyperLink NavigateUrl='<%# DataBinder.Eval (Container.DataItem, "Url") %>'>
<%# DataBinder.Eval (Container.DataItem, "Title") %>
I hope this helps to bind Sharepoint to ASP.NET quickly and painlessly. :)
Databinding is cool. It lets us connect our front-end GUI elements (say, a DataGrid) to our back-end data elements (a DataSet, for instance) without the drudgery of synchronizing the two all the time. True, ASP.NET Datagrids are one-directional, but it's still cool.
The nice thing about databinding is that it can be done with custom objects, not only DataSets and such. Specifically, a DataSet's DataSource can be any object that implements ICollection. Let's see two ways to use this to bind a datagrid to a group of SPListItems:
1) Binding to a SPListItemCollection - the straightforward way.
The binding itself is a snap - if we have an SPListItemCollection object (from an SPList's Items property, for instance) we can simply set it as the DataSource of our grid - it implements ICollection and all is well:
SPListItemCollection items = myList.Items;
dgItems.DataSource = items;
The problem, however, is with the Binding code in our ASPX page. We can bind to properties of the SPListItem object, but not to indexed properties. Code such as this will work, and bind to the current item's ID property:
<%# DataBinder.Eval (Container.DataItem, "ID") %>
But how do I bind to myListItem["Title"] or myListItem["MyCustomProperty"]?
One thing that's easy to forget is that we can write normal .NET code inside the <%# %> blocks, even if the lack of Intellisense seems otherwise. So we can cast our DataItem to an SPListItem and get the property:
<%# ((SPListItem)Container.DataItem)["MyProperty"] %>
This requires a few additions to the top of the ASPX:
<%@ Import Namespace="Microsoft.SharePoint" %> <!-- This is so we can reference SPListItem without the full namespace -->
<%@ Assembly Name=""Microsoft.SharePoint, Version=188.8.131.52, Culture=neutral, PublicKeyToken=71e9bce111e9429c"" %>
Note the second line. This is because the code in our embedded script blocks doesn't use the references from the main project, and we have to manually specify the Sharepoint assembly reference.
Using the same concept, we can bind to any property - or even method call results - of any custom object we have.
I have this class, that implements the singleton pattern, and a subclass. public class Base
public static Base Instance;
System.Type myType = MethodInfo.GetCurrentMethod().DeclaringType;
Instance = (Base)Activator.CreateInstance(myType);
public void ID()
MessageBox.Show("I am " + this.GetType().Name);
} public class Sub : Base
When I call Base.Instance.ID(), I get "I am Base", which is what I want.
When I call Sub.Instance.ID(), I also get "I am Base". This makes sense,
since MethodInfo.GetCurrentMethod().DeclaringType SHOULD return Base, since that's where it's declared.
The question is - is there any way to tell, when Base's static constructor is called - which object I am, in fact, instantiating? Or, since it's static, there really isn't any Sub object to get info from?
When we write a WebPart or WebControl that resides in a SPS Area, we will want to know at runtime which area we reside in.
This, surprisingly enough, is not trivial. It's not even easy. In fact, it's downright obscure
You can easily get the current SPWeb you're in using the SPControl object, but while the Area class can expose the underlying SPWeb, an SPWeb can't give you the equivalent Area - which makes sense, since SPS's Areas are built on top of WSS's SPWebs.
This data is stored quite nicely in the database - there's a WebCat table that cross-references Area GUIDs with Web GUIDs - but we really don't want to access the DB directly, do we?
The (obscure) solution is this:
From the Context property of the webpart/control, access the stored PageInfo object stored in the hashtable:
PageInfo pi = (PageInfo)Context.Items["SPS_PageInfo"];
The PageInfo class is defined in Microsoft.SharePoint.Portal.WebControls namespace, and is apprently undocumented in any SDK I found. It contains a property named CategoryID, which is the Guid of the current Area:
Guid currAreaGuid = pi.CategoryID;
Once we have the Guid, it's clear sailing all the way:
Area currArea = AreaManager.GetArea(PortalContext.Current, currAreaGuid);
And there was much rejoicing.
Having found myself developing in VB.NET alongside C# recently, I was rather annoyed at the lack of pre- and post-build-events for VB projects in VS.NET2003.
A quick check of the DTE automation model showed that while the .VBPROJ file schema does not enable persistently storing build event information for VB projects, the EnvDTE.Project object DOES enable you to specify that property, whether it's a C# or VB project - they just won't be persisted to the VBProj file.
Inspired by Roy's recent Add-In contest, I decided to write a plug-in that persisted the build events to custom user properties in the project file and then reloads them into the PostBuildEvents on project load. A quick googling, however, revealed that I am, alas, too late. Microsoft themselves have released this add-in as part of their Automation Samples for extending Visual Studio.
I shall but link, and grieve.