- Design
- Code
- Implementation (Future Topic)
Overview
In Part 1 I discussed the design of the Global Navigation that I recently implemented. It may not be applicable for every situation but it worked well for that particular project. This time I will go into some code snippets of how I queried the object model for the site collections and sites that the current user was authorized to.
Design Review
A quick review of the design and some elaboration on it as we proceed. Note that for this post I will be focusing on the dynamic menu elements because those are the most interesting from a code standpoint. The menu will have two dynamic top level menu items:
- Projects
- Communities
This post will focus on the Communities and Project menus because those are dynamic. Also I will not be going through every line of code from this project, I pulled out snippets of some of the more interesting bits and I rewrote those to make sense when pulled out of context.
Farm Topology
As everyone hopefully knows a SharePoint farm is broken down into Web Applications, Site Collections, and Sites. The project architect decided that the topology will be as follows:
- Collaboration Web Application
- Test Community A Site Collection
- Test Community B Site Collection
- Internal Projects Site Collection
- Internal Project 1 Site
- Internal Project 2 Site
- Customer Projects Site Collection
- Project 3 Site
- Project 4 Site
Querying Sites under a Site Collection
In my example to get a list of Project sites (Internal or Customer) required querying for all Sites within a Site Collection.
struct TempMenuData
{
public string Title;
public string Url;
public DateTime Created;
}
....
SPSecurity.RunWithElevatedPrivileges(delegate {
// Temp storage
var tempList = new List();
/*
* Note: creating the SPSite with SPUserToken.SystemAccount is required even though
* this block is being run as a delegate within RunWithElevatedPrivileges.
*
* This is contrary to what the API doc implies, removing this will cause the * sub.DoesUserHavePermissions() to error with an authorization exception
*/
using (SPSite siteCollection = new SPSite(siteUrl, SPUserToken.SystemAccount))
{
using (SPWeb rootWeb = siteCollection.OpenWeb())
{
// retrieve only Sub-sites that current user is a member of
foreach (SPWeb site in rootWeb.Webs)
{
if ( site.DoesUserHavePermissions( currentUserLogin, SPBasePermissions.ViewPages | SPBasePermissions.ViewFormPages | SPBasePermissions.ViewListItems))
{
tempList.Add(
new TempMenuData
{
Title = site.Title,
Created = site.Created,
Url = site.Url
}
}
}
}
}
IOrderedEnumerable orderedQuery = tempList
.OrderByDescending(o => o.Created)
.OrderBy(o => o.Title);
}
There is a lot going on here and formatting the code to fit in the blog is not necessarily the best way to view the code but I'll break it down.
- Start by running everything in a RunWithElevatedPrivileges delegate
- Open up the SPSite using the SPUserToken.SystemAccount token
- Open up and iterate through each SPWeb within this SPSite
- For each site that is found verify that the current user has sufficient privileges to view the site using DoesUserHavePermissions()
- The various SPBasePermissions are the exact permissions the user must have to be considered authorized to have this site in their global navigation
- The temporary list of menu data is then sorted into a IOrderedEnumerable which is used later to actually generate the menu
Most noteworthy topic is that both RunWithElevatedPrivileges and SPUserToken.SystemAccount are required to properly search the farm. This is because the menu could be called by any user (anonymous, domain user, site admin, farm admin), in my testing the only way to ensure the code worked regardless of the user was to use both techniques. This was confusing enough that I included a block comment reiterating this so no one ever forgot why this was done that way.
Querying Site Collections with a Web Application
The other example involves extracting the list of Communities out of the Collaboration web application.
struct TempMenuData
{
public string Title;
public string Url;
public DateTime Created;
}
....
SPSecurity.RunWithElevatedPrivileges(delegate
{
string subQueryPath = "/communities/";
// Temp storage
var tempList = new List();
SPWebApplication webApp = SPWebApplication.Lookup(new Uri(webAppUrl));
foreach (SPSite s in webApp.Sites)
{
/*
* Note: creating the SPSite with SPUserToken.SystemAccount is required even though
* this block is being run as a delegate within RunWithElevatedPrivileges. Since the
* SPUserToken can not be specified in the SPWebApplication declaration or following
* webApp.Sites call a new SPSite must be instantiated so the proper Token can be
* provided.
*
* This is contrary to what the API doc implies, removing this will cause the
* RootWeb.DoesUserHavePermissions() to error with an authorization exception
*/
using (SPSite s_sys = new SPSite(s.Url, SPUserToken.SystemAccount))
{
if (
s_sys.RootWeb.Url.Contains(subQueryPath) &&
s_sys.RootWeb.DoesUserHavePermissions( currentUser.LoginName, SPBasePermissions.ViewPages | SPBasePermissions.ViewFormPages | SPBasePermissions.ViewListItems))
{
tempList.Add(
new TempMenuData
{
Title = s_sys.RootWeb.Title,
Created = s_sys.RootWeb.Created,
Url = s_sys.RootWeb.Url
});
}
}
}
orderedQuery = tempList .OrderByDescending(o => o.Created) .OrderBy(o => o.Title);
}
This code is very similar to the other snippet
- Start by running everything in a RunWithElevatedPrivileges delegate
- Create a subQueryPath variable – This is the managed path for community site collections
- Create up the SPWebApplication to be queried
- For each SPSite in this SPWebApplication reference that site using the SPUserToken.SystemAccount
- Check that this SPSite
- Is using the specified managed path
- That the current user is authorized to this site
- The temporary list of menu data is then sorted into a IOrderedEnumerable which is used later to actually generate the menu
Again you will notice the logic of this snippet is very similar to the other, there are some notable differences however. First is that the SPSite needs to first be found within the SPWebApplication, then this SPSite needs to be re-created with the SPUserToken.SystemAccount, this is because the SPWebApplication API did not have a way of providing the user token directly. This is an indirect way of accomplishing this but it was the only way I could find.
The other note is that we needed to provide what I called the "subQueryPath", this was my way of filtering what site collections were included in this menu. That was required because if you look at the topology the collaboration web application included community and project site collections, however the menu needed them broken out. Since these different type of collections were using different managed paths I simply check the url of the site to make sure it included "/communities/" (ie. the managed path) before attempting to include it in the menu. This is hard coded in the above snippet but the actual code is more dynamic because this is obviously a bad practice to hard code a value such as this.
Final Thoughts on Code
There were some specific security techniques that were required to ensure that any user would be able to get their personalized menu correctly. This is very tightly related to the next topic that I'm calling "Implementation" because the farm needs some specific configurations so that the above code can do it's thing. The security was the biggest challenge for this project because figuring everything out involved debugging the development server environment and pouring through ULS and Event Log entries. Save yourself some agony and check out all three parts of this blog to see how I set everything up.









