Friday, March 31, 2006

TechEd 2006 SharePoint Sessions

If you are planning to attend TechEd 2006 in Boston then you should drop by the web site and vote for the SharePoint Birds of a Feather sessions.  Heather Solomon and Andrew Connell each have one and and Bil Simser has several (and if you don’t already read their blogs then get over there now and add them to your newsreader pronto).  Don’t let the SQLers get all the exposure – show ‘em we want us some SharePoint!!!

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.

Saturday, March 18, 2006

SharingPoint Update

You may have noticed that I haven’t been blogging much lately. I’ve been swamped with projects, starting with a complete, top-to-bottom site customization at the beginning of the year that I completed in just two weeks, followed by a month in Indianapolis in February, a few days in Baltimore, then right on to a project in Dallas. In between I redesigned a company web site for a friend, did my corporate taxes, and emptied a storage bin full of old stuff. Whew!

During this time I’ve been doing A LOT of custom development in .NET – web parts, object model code, server controls, custom lists, the whole nine yards. As most people know I’m not the greatest programmer in the world and only know enough C# to be really dangerous so it’s been quite the learning experience. Here’s a sample of what I’ve been up to:

  • Dynamic drop-down portal area navigation server control

  • Custom list query/XML rendering administration pages

  • Visio design template for SPS/WSS deployments

  • Custom site navigation control/web part with security trimming

  • Advanced customization best practices document

  • Document library event handlers (not started yet)

  • List search pages
As is my custom, I’ll be posting detailed coding techniques, tips, tricks and sample code over the next few weeks. Stay tuned for some good stuff!

Wednesday, March 01, 2006

Extreme SharePoint Design: Modifying 'Grouped By' Headers, Part 2

Creating a grouped list in SharePoint is easy – just Modify Settings and Columns, Create View, and select the column(s) to group on; however, getting the grouping to sort and display the way you want isn’t quite so simple. By default, SharePoint sorts alphabetically and there aren’t any options to hide group headers, style them individually, or change the layout.

One of the most common requirements for grouped listings is to create a manual sort mechanism to circumvent the built-in alphabetical sort. This is easily done by adding a new field with a numerical value and assigning each list entry to the appropriate identifier. When a new view is created, the list is first grouped by the sort field and next by whatever content field is appropriate. The problem is that the user sees the sort field in the first group header and there’s no way to turn it off in the list settings.

There are two ways to solve this problem. You can modify the SCHEMA.XML file for the list type and change the CAML to render the groupings the way you want them (read this post to learn how). The issue with this method is that it is a global change; all lists of the same type in the site definition will be changed. Alternatively, you can override the default styles using a web part which hides or alters the appearance of the stock group header on a page by page basis.

Each header level is contained within a table row that inherits the ‘ms-gb’ (first group header) or ‘ms-gb2’ (second group header) style from OWS.CSS. To override these styles, add a Content Editor Web Part (CEWP) to the page, open the Source Editor in the tool pane, and add your own stylesheet entry. For example, to hide the first group header row entirely (dark gray bar in default theme), enter the following:

<style>
.ms-gb{display:none}
</style>

Disable the CEWP’s ‘Display on page’ setting (under ‘Layout’ in the tool pane) to hide the web part, save it and the group header disappears. Because the CEWP is rendered after the default stylesheet references in the site definition page, it overrides the settings in OWS.CSS following the cascading rules of CSS (last reference takes precedence). Technically, the header still exists, users just can’t see it. Use the same method to apply font colors, background images, margins, and any other CSS style properties.

Unfortunately, this method doesn’t work as well for altering the actual text of the header entry (i.e. removing the ‘[Group Name] :’ prefix). You can use JavaScript to achieve this effect by altering the innerHTML property of each header and using the getElementById method, but you’ll have to apply it individually to each header ID and they’re not always the same from page to page. Plus, JavaScript in CEWP’s often causes unpredictable results (such as preventing web parts from being saved or disabling tool bar functions). If you do plan to use JavaScript on a web part page, it’s always best to store the script in a document library as a .js file or as part of an HTML page then call that page using a Page Viewer Web Part.