Office Space

Security Programming in SharePoint 2007

Ted Pattison

Code download available at:OfficeSpace2008_02.exe(209 KB)

Contents

External Security Principals and the SPUser Object
Adding Authenticated and External Users
Working with Permission Levels
WSS Groups
Identity, Elevation, and Impersonation
Securable Objects
Wrapping Up

You may already know the fundamentals of security programming with Windows® and ASP.NET security, but how well do you know the security layer that Windows SharePoint® Services 3.0 (WSS) adds on top? In this month's installment of Office Space, I will highlight new security terms and concepts that are introduced with WSS, and I will give you a jumpstart into the world of security programming using the WSS object model.

I recommend that you download the sample project that accompanies this column and follow along with the code I will present throughout the rest of this column. The project has been configured to run a batch file after the build process, which will compile all of the project components into a WSS solution package and install the package on the local WSS farm. Once you have built the project and installed the solution, you can navigate to any site collection and enable the feature named Security Demo, which is scoped to the site collection. The Site Actions menu will then allow you to navigate to custom application pages, which have code behind them demonstrating WSS security programming techniques.

External Security Principals and the SPUser Object

Most security models are based on security principals. Each security principal represents either a user or a group. Users have accounts against which they authenticate. Once authenticated, each user takes on an identity. In the scenario where a user has authenticated against a Windows account, you can use the Microsoft® .NET Framework security classes in the System.Security namespace to retrieve an identity that points back to the specific Windows account and allows you to discover the user's login name:

WindowsIdentity identity = WindowsIdentity.GetCurrent(); 
string WindowsLogin = identity.Name;

Using a WindowsIdentity, you can dynamically create a WindowsPrincipal, which will let you perform a test to see whether the current user is a member of an Active Directory® group or a local Windows group, like so:

WindowsIdentity identity = WindowsIdentity.GetCurrent(); 
WindowsPrincipal principal = new WindowsPrincipal(identity); 
if( principal.IsInRole(@"LITWAREINC\AllFTE") ){ 
  // perform operation allowed for fulltime employees
}

ASP.NET supports both Windows authentication and Forms-based authentication (FBA). The User object in ASP.NET abstracts away the dependency on Windows accounts by modeling the User object on the IPrincipal interface instead of the WindowsPrincipal class. The ASP.NET runtime dynamically creates different types of IPrincipal objects depending on whether the current user has authenticated using a Windows account or an FBA account:

IPrincipal AspUser = HttpContext.Current.User;
string AspUserName = AspUser.Identity.Name;

The ASP.NET User object also provides a way to check to see if a user belongs to a particular role using the IsInRole method. For Windows users, the IsInRole method lets you see whether the current user is a member of an Active Directory group. If you are using FBA accounts along with an ASP.NET Role Provider, you can also use the IsInRole method to check whether the FBA user has been added to a specific ASP.NET role:

IPrincipal AspUser = HttpContext.Current.User; 
if(AspUser.IsInRole("Site Administrators") { 
  // perform privileged operation 
}

The act of authentication produces some form of a receipt that is used by the system at run time to represent a user identity and to track membership in groups or roles. When a user authenticates against a Windows account, the receipt of authentication is a Windows security token. When the user authenticates against an FBA account, the receipt of authentication is an HTTP cookie created by the ASP.NET runtime and a specific authentication provider.

It's important to understand that WSS does not provide support for authenticating users. Instead, WSS makes use of underlying authentication components supplied by the various ASP.NET authentication providers. The value that WSS brings to securing a site has to do with configuring authorization and access control. WSS makes it possible to track external security principals‑such as Windows users, FBA users, Windows groups, and ASP.NET roles‑within the scope of a site collection. With WSS, you can also configure the permissions that are assigned to these external principals, which, in effect, grant users permissions to access securable WSS objects such as sites, lists, items, and documents.

Note that the site collection plays a significant role when you're configuring authorization and access control. WSS considers each site collection to be its own separate island when it comes to tracking external principals and configuring permissions. The high-level design of WSS intentionally isolates each site collection so that a user's security settings in one site collection do not impact permissions or access control policy in another site collection.

The WSS object model represents external security principals using SPUser objects. You can retrieve the SPUser object for the current user through the current SPWeb object:

SPWeb site = SPContext.Current.Web; 
SPUser user = site.CurrentUser; 
string DisplayName = user.Name; 
string Login = user.LoginName; 
string EMail = user.Email; 
string User Notes = user.Notes;

The SPUser object exposes properties of an external security principal, such as login name, display name, and e-mail address. These properties are usually retrieved from an underlying user repository, such as an Active Directory domain, when the external principal is added to a site. The SPUser object also exposes properties that track WSS-specific metadata, such as the Notes field.

WSS maintains the profile data for external users, groups, and roles in a hidden list, which is known as the User Information List. Every time WSS provisions a new site collection, it automatically creates the User Information List as a hidden list in the top-level site. WSS then adds a new profile for each external principal the first time that principal is assigned permission or the first time it passes a security check to access a securable object. Note that the user profile stored in the User Information List does not extend across site collections—when users update their profile settings in one site collection, there are no changes to that user's profile settings in other site collections.

Another potential source of confusion is that SPUser objects do not always represent actual users. SPUser objects can also represent Active Directory groups and ASP.NET roles. WSS tracks a profile for each of these external principal types in the User Information List along with the profile data for external users.

Many of the programmatic aspects of the SharePoint security model are exposed at the site level through SPWeb objects. This is the case if you want to discover which users are members of the current site. An SPWeb object exposes three different collections of users, as shown in this code fragment:

SPWeb site = SPContext.Current.Web; 
SPUserCollection c1 = site.Users; 
SPUserCollection c2 = site.AllUsers; 
SPUserCollection c3 = site.SiteUsers;

The Users collection has the smallest membership of these three collections. This collection includes all the external principals that have been explicitly assigned permissions within the current site.

The AllUsers collection includes all members of the Users collection, plus external users that have accessed objects within the site using implicit permissions through group or role membership. For example, imagine a user named Brian with the login of LITWAREINC\BrianC that has never been given explicit permissions to access a site and view a particular list. However, he might still be able to view the list because of his membership within an Active Directory group that has been configured with list view permissions. When Brian first accesses the site or one of its objects (say, a list using implicit permissions), he is added as a member of the AllUsers collection, but he is not added as a member of the Users collection.

The SiteUsers collection is an aggregation that combines membership for each AllUsers collection within the current site collection. The membership of this collection includes all external principals that have been assigned permissions to any object within the site collection as well as all external users that have been granted access to any of the site collection's objects using implicit permissions.

Adding Authenticated and External Users

So how do you create a new WSS user profile for a user that has authenticated using an Active Directory account? If you need to create a custom user interface component that allows standard users or the site collection owner to select a user or group from an Active Directory domain, you really should learn how to use the PeoplePicker control (see Figure 1). This is a handy, reusable control type that ships with WSS. You can add this control to a custom application page or a User control using a control tag that looks like this:

Figure 1 PeoplePicker Control

Figure 1** PeoplePicker Control **(Click the image for a larger view)

<SharePoint:PeopleEditor ID="pickerPrincipal" 
  AllowEmpty="false" ValidatorEnabled="true" 
  MultiSelect="false" SelectionSet="User, SecGroup, DL" 
  Width="280px" runat="server" />

In this sample, I have configured the PeoplePicker control by assigning the SelectSet property with values of User, SecGroup, and DL. These SelectSet settings configure the control to allow the user to select and resolve a user, group, or distribution list against Active Directory.

You can programmatically access the PeoplePicker control properties to retrieve the associated login account names for the underlying accounts after the user has selected one or more security principals with the control. Then you can supply the code that actually adds these principals as site members and configures their access rights.

Now I should show you how to add an external user or group as a site member. After a brief look at the WSS object model, you might think that you should simply add external security principals directly into one of the SPUser collections, such as SiteUsers:

SPWeb site = SPContext.Current.Web; 
site.SiteUsers.Add(@"LITWAREINC\BrianC", "brianc@litwareinc.com",  
  "Brian Cox", "Notes about Brian Cox"); 
site.SiteUsers.Add(@"LITWAREINC\AllFTE", 
  "allFTE@litwareinc.com", "All Full-time Employees", "Notes about FTE DL");

While this approach does create a profile in the User Information List for an external principal, it has little effect on security since it doesn't assign a permission. A better way to add a new external security principal involves assigning permissions so that users have access within the current site. However, you first need to learn how to create and assign permission levels.

Working with Permission Levels

A permission level is a named set of permissions that is defined within the scope of a site. WSS includes four built-in permission levels: Read, Contribute, Design, and Full Access. If you need greater granularity than this, you can create your own custom permission levels using the WSS object model or through the standard WSS administrative pages that are accessible to the site collection owner.

Permission levels are sometimes called roles, and they are represented in the WSS object model using SPRoleDefinition objects. You can assign a permission level to an external user or groups using a SPRoleAssignment object. For example, here I assign the built-in Contribute permission level to the Windows user with a login name of LITWAREINC\BrianC:

SPWeb site = SPContext.Current.Web; 
SPRoleDefinition role = site.RoleDefinitions["Contribute"]; 
SPRoleAssignment roleAssignment; 
roleAssignment = new SPRoleAssignment(@"LITWAREINC\BrianC", "brianc@litwareinc.com", 
  "Brian Cox", "Notes about Brian Cox"); 
roleAssignment.RoleDefinitionBindings.Add(role); 
site.RoleAssignments.Add(roleAssignment);

This technique makes it unnecessary to add the user to one of the SPUser collections since that is done automatically by WSS when an external user or group is assigned a permission for the first time within a site. The code you've just seen will create a user profile in the User Information List if one does not exist, and it will also add the user as a member of the current site's Users collection.

WSS Groups

While the WSS security model represents external security principals as SPUser objects, it also provides WSS groups as a means to ease the configuration of permissions within the scope of a site collection. For example, you can design a set of WSS groups within a site collection for specific user roles, such as Site Members, Content Manager, and Site Administrators. Once you have done this, you can configure the site's security settings by simply assigning permission levels to WSS groups instead of assigning permission levels directly to SPUser objects.

The obvious benefit to creating WSS groups is that they help to eliminate the ongoing need to reconfigure permissions as new external users and groups are added and removed. Instead, you can configure permissions once and for all when the site is created. Then, moving forward, you simply add and remove external users and groups to and from WSS groups. WSS groups really follow the exact same design principles as Active Directory groups, with the main difference being that WSS groups are defined and exist only within the scope of a single site collection.

WSS groups are represented in the WSS object model as SPGroup objects. The SPWeb object exposes two collections of SPGroup objects, named Groups and SiteGroups. The Groups collection includes all the WSS groups that have been directly assigned permission in the current site, while the SiteGroups collection is a super set of the Groups collection and includes all of the WSS groups that have been created inside the current site collection.

When you want to create a new WSS group, you should call the Add method exposed by the SiteGroups collection and then assign the new WSS group with one or more permission levels within a target site. Figure 2 shows an example of creating a new WSS group named Site Members and assigning it the built-in Contribute permission level within the current site.

Figure 2 Creating a New WSS Group

SPWeb site = SPContext.Current.Web; 
SPUser currentUser = site.CurrentUser; 
// create new group site.
SiteGroups.Add("Site Members", currentUser, currentUser, 
  "Site Group created at " + DateTime.Now.ToString()); 
// assign permission level to new group 
SPGroup NewGroup = site.SiteGroups["Site Members"]; 
SPRoleAssignment roleAssignment = new SPRoleAssignment(NewGroup); 
SPRoleDefinition permLevel = site.RoleDefinitions["Contribute"]; 
roleAssignment.RoleDefinitionBindings.Add(permLevel); 
site.RoleAssignments.Add(roleAssignment);

 

Once you have created a new WSS group, it is pretty easy to add external users and groups as members. An SPGroup object exposes an AddUser method, which accepts an SPUser object, which in turn allows you to add external users and groups:

SPWeb site = SPContext.Current.Web; 
SPUser currentUser = site.CurrentUser; 
SPGroup group = site.SiteGroups["Site Members"]; 
SPUser user1 = site.SiteUsers[@"LITWAREINC\BrianC"]; 
SPUser user2 = site.SiteUsers[@"LITWAREINC\AllFTE"]; 
group.AddUser(user1); group.AddUser(user2);

Identity, Elevation, and Impersonation

The worker process for a WSS Web application is controlled through IIS Application Pools. The worker process identity for a Web application warrants your attention and is configurable through the SharePoint Central Administration application. You should configure the worker process identity for a Web application in terms of domain accounts (such as LITWAREINC\SP_WorkerProcess) instead of relying on local accounts (such a NETWORK SERVICE).

Note that the worker process identity for a Web application must be a privileged Windows account that has been configured with SQL Server permissions to read and write to one or more content databases. The worker process identity for the Web application running the SharePoint Central Administration site must be even more privileged since it requires read/write permissions to the farm's configuration database.

When the code behind a Web Part or a custom application page executes in response to a user request, the code does not execute under the worker process identity of the hosting Web application. Instead, WSS uses impersonation to switch the Windows security context over to another Windows account. In fact, if you look inside the web.config file for a WSS Web application, you will see the following entry:

<configuration> 
  <system.web> 
    <identity impersonate="true" /> 
  </system.web> 
</configuration>

When a request is executed for a user that has been authenticated against a Windows account, the request impersonates the current user's Windows identity. However, the same approach is not possible for FBA users because FBA authentication does not create a Windows security token and has no Windows identity. Therefore, requests for users running under FBA authentication impersonate the identity of the Windows account that has been configured for anonymous access. The default assignment for this account in IIS is the IUSER_MACHINENAME account, but you can—and typically should—reconfigure this to point to a domain account.

Now it's time to step back and view WSS security programming at a higher level. The WSS security model often forces developers to differentiate between Windows identity and WSS user identity. This might not be so obvious within a request where the current Windows identity and the current WSS user identity both point to the same Windows login. However, in scenarios that utilize FBA, things get a bit more complicated. For example, the WSS user identity could point to an FBA user named Andrew while the underlying Windows identity is based on the IUSER_MACHINENAME account. As your code attempts to access WSS objects, WSS runs access checks using the user's WSS identity. And as your code attempts to access external objects outside of WSS, such as files maintained by the Windows operating system, the OS runs access checks using the Windows identity under which the code is currently executing.

There are times when you need your code to execute with greater permissions than is possessed by the current user. For example, imagine a scenario in which your code must write data to a list when processing a request for a user that has nothing more than read-only permissions. By default your code runs with the same permissions as the current user. However, you can make a call to the RunWithElevatedPrivileges method of the SPSecurity class to elevate the security context of your code. Note that a call to RunWithElevatedPrivileges elevates both the WSS user identity and the Windows identity.

Now imagine a scenario where a user has authenticated against a Windows account with a login of LITWAREINC\BrianC. A call to RunWithElevatedPrivileges will elevate the WSS user identity to the SHAREPOINT\System account. The SHAREPOINT\System account is built into the WSS runtime and is all powerful within the WSS authorization model. A call to RunWithElevatedPrivileges will also switch the Windows identity of the executing code so it runs under the worker process identity of the current Web Application:

// BEFORE ELEVATION 
// WSS User identity = LITWAREINC\BrianC 
// Windows identity = LITWAREINC\BrianC SPSecurity.RunWithElevatedPrivileges(delegate() { 
  // AFTER ELEVATION 
  // WSS User identity = SHAREPOINT\System 
  // Windows identity = LITWAREINC\SP_WorkerProcess });

In some scenarios, you might choose to call the RunWithElevatedPrivileges method to change the Windows identity of the current call before you attempt to access a file inside the Windows file system or a SQL Server database. Also note that switching the Windows identity over to a process identity such as LITWAREINC\SP_WorkerProcess can eliminate the need to configure delegation within an Active Directory environment. This can be very valuable if you have custom Web Parts that access data in a remote SQL Server database using Windows integrated authentication.

And then there are other scenarios where you might want to call the RunWithElevatedPrivileges method to elevate the WSS user identity to SHAREPOINT\System so that your code can perform operations not allowed by the current user. Once your code is running as SHAREPOINT\System, you can do pretty much anything you want within the WSS authorization subsystem.

There is one tricky aspect of calling RunWithElevatedPrivileges to elevate to the SHAREPOINT\System account. For example, imagine you call RunWithElevatedPrivileges and then you attempt to access objects in the current site collection or site through the SPContext.Current property. You probably wouldn't expect your code to fail, but it might:

SPSecurity.RunWithElevatedPrivileges(delegate() { 
  SPSite siteCollection = SPContext.Current.Site; 
  // next line fails if current user is Contributor 
  string siteCollectionOwner = siteCollection.Owner; });

Why does this sample code fail after you have elevated the WSS user identity to SHAREPOINT\System? It has to do with when the SPSite object was created. The permissions in effect on an SPSite object and its child SPWeb objects do not depend on the current WSS user identity. Instead, the permissions depend on the WSS user identity at the time the SPSite object is created. Here, the SPSite object accessible through SPContext.Current was created earlier in the request, before your code had a chance to switch its WSS user identity.

Thus, the technique you should use requires that you create a new SPSite object after you have called RunWithElevatedPrivileges and elevated WSS user identity to SHAREPOINT\System:

SPSecurity.RunWithElevatedPrivileges(delegate() { 
  using (SPSite elevatedSiteCollection = new SPSite(this.Site.ID)) { 
    using (SPWeb elevatedSite = elevatedSiteCollection.OpenWeb(this.Web.ID)) { 
    // access elevatedSiteCollection and 
    //elevatedSite as SHAREPOINT\System } 
    } 
  });

This makes it possible to open a site collection and the sites within so that your code can access objects as SHAREPOINT\System.

You might also find it necessary to impersonate a specific WSS user identity. This is common when writing the code for an event handler or a custom workflow template where the code is running as SHAREPOINT\System by default. For example, you might want to impersonate a specific WSS user identity before creating a new object so that WSS user is recognized as the owner of the new object.

In order to impersonate a WSS user identity, you must first create an SPUserToken object. You can do this by accessing the UserToken property of an SPUser object. Once you have the SPUserToken object, you can use it to create a new SPSite object using an overloaded version of the SPSite class constructor. This technique is shown in Figure 3.

Figure 3 Impersonating a WSS User Identity

SPWeb siteCollection = SPContext.Current.Site; 
SPWeb site = SPContext.Current.Web; 
// get SPUser object and acquire token 
SPUser targetUser = site.SiteUsers[@"LITWAREINC\BrianC"]; 
SPUserToken token = targetUser.UserToken; 
// create new SPSite and SPWeb object to impersonate user 
using (SPSite impersonatedSiteCollection = new SPSite(siteCollection.ID, token)) { 
  using (SPWeb impersonatedSite = impersonatedSiteCollection.OpenWeb(site.ID)) { 
    // WSS identity switched to impersonate BrianC 
    // Windows identity does not change 
  }
}

 

There are a few important things I should point out about using WSS user impersonation. First, impersonating a WSS user is different than a call to RunWithElevatedPrivileges because it does not change the current Windows identity. If, for example, a request is running under the Windows identity of LITWAREINC\SP_WorkerProcess before you impersonate a WSS user, the code continues to run under the same Windows identity. WSS user impersonation does not change the current Windows identity over to the identity of the impersonated user.

It is also important to point out that code must be running in a privileged state in order to impersonate another user. This isn't something you have to worry about in an event handler or a custom workflow template since the code is running as SHAREPOINT\System by default. However, code inside a Web Part or behind a custom application page might need to call RunWithElevatedPrivileges before it even has the ability to impersonate another WSS user identity.

Securable Objects

The real power in configuring WSS security involves the flexibility afforded by securable objects, such as sites, lists, and list items. Each securable object can contain an Access Control List (ACL), which is a binary data structure that WSS uses at run time to determine whether a security principal has been granted access. By default, the only securable object that has an ACL is the top-level site. Child objects (such as list, list items, and child site) all inherit the ACL of their parent unless they break the inheritance and thereby provide a unique ACL of their own.

The WSS object model contains an interface named ISecurableObject that models a securable object within a WSS site collection (see Figure 4). The ISecurableObject interface, which is implemented by SPWeb objects, SPList objects, and SPItem objects, provides a basis for conducting access checks at run time as well as for configuring permissions.

Figure 4 The ISecurableObject Interface

Figure 4** The ISecurableObject Interface **(Click the image for a larger view)

As you begin to configure permissions within a site collection, it's important to understand that all of the sites, lists, and list items make up a single hierarchy of securable objects. By default, just the top-level site contains a unique ACL and defines permission-level assignments that dictate what permissions users have to access objects. All child objects inherit their permissions from the top-level site. However, you can be more granular about configuring access control by giving a securable object its own unique set of permission-level assignments. For example, the code shown in Figure 5 allows you to create a new document library and to configure it with a unique set of permissions.

Figure 5 Configuring a Unique Set of Permissions

SPWeb site = SPContext.Current.Web; 
Guid listID = site.Lists.Add("Proposals", "Library desc", 
  SPListTemplateType.DocumentLibrary); 
SPDocumentLibrary doclib = (SPDocumentLibrary)site.Lists[ListID]; 
doclib.OnQuickLaunch = true; 
doclib.BreakRoleInheritance(false); 
SPUser AllFteGroup = Web.SiteUsers[@"LITWAREINC\AllFTE"]; 
SPRoleAssignment assignAllFteGroup = new SPRoleAssignment(AllFteGroup); 
SPRoleDefinition roleDesign = this.Web.RoleDefinitions["Read"]; 
assignAllFteGroup.RoleDefinitionBindings.Add(roleDesign); 
doclib.RoleAssignments.Add(assignAllFteGroup); 
doclib.Update();

This sample code breaks the default permission inheritance from the parent using a call to BreakRoleInheritance. If you call BreakRoleInheritance and pass a parameter value of true, the securable object is initially configured with an ACL that is a copy of the parent object's ACL. If you call to BreakRoleInheritance and pass a parameter value of false, the securable object is initially configured with an empty ACL. That means this document library provides no access to users who are not either owners or site administrators.

Windows SharePoint Services 3.0 has added a welcome security enhancement that allows you to configure permissions down to the level of the item or document. This is made possible through the WSS object model, since SPListItem objects also implement the ISecurableObject interface.

The code in Figure 6 creates a new document within a document library and then configures it with a unique set of permissions that differ from its parent document library. Note that this code uses a utility method, called WriteDocument, that accepts an SPDocumentLibrary reference and a file name. The method implementation uses the Office Open XML File Formats to create a Word document and to write it back to the document library. The WriteDocument method returns an SPFile reference, which can then be used to access the SPListItem associated with the document, allowing you to break permission inheritance and assign a unique set of permissions.

Figure 6 Set of Permissions that Differs from Its Parent

SPWeb site = SPContext.Current.Web; 
Guid listID = site.Lists.Add("Proposals", "Library desc", SPListTemplateType.DocumentLibrary); 
SPDocumentLibrary doclib = (SPDocumentLibrary)Web.Lists[ListID]; 
doclib.OnQuickLaunch = true; doclib.Update(); 
SPFile doc1 = WriteDocument(doclib, "Adventure Works Merger.docx"); 
doc1.Item.BreakRoleInheritance(false); 
SPGroup group = Web.Groups["Litware Contact Managers"]; 
SPRoleAssignment assignContribute = new SPRoleAssignment(group); 
SPRoleDefinition roleContibute = this.Web.RoleDefinitions["Contribute"]; 
assignContribute.RoleDefinitionBindings.Add(roleContibute); 
doc1.Item.RoleAssignments.Add(assignContribute); 
doc1.Item.Update();

Figure 6 Set of Permissions that Differs from Its Parent

Wrapping Up

I know that this discussion has been somewhat of a whirlwind tour through the security model of WSS. I've shown you how WSS tracks external security principals at the level of the site collection with a profile within the User Information List and explained how WSS represents these external security principals in the WSS object model using SPUser objects. I've also demonstrated how WSS provides support for WSS groups and presented a few programming techniques for elevating privileges and impersonating WSS users. These techniques provide the power and flexibility you need when creating a real-world application.

While WSS relies on an underlying system of components to perform authentication, it does take on the responsibility of authorization and access control. The WSS authorization model is largely based on a named set of permissions known as permission levels or roles. A permission level can be assigned to an SPUser object, but in practice you should generally choose to assign permission levels to WSS groups.

Send your questions and comments for Ted to mmoffice@microsoft.com.

Ted Pattison is an author, trainer, and SharePoint MVP who lives in Tampa, Florida. He also delivers advanced SharePoint training to professional developers through his company, Ted Pattison Group (www.TedPattison.net). Ted has just completed his book titled Inside Windows SharePoint Services 3.0 for Microsoft Press.