December’s looking a bit bare on my blog calendar. I thought I’d better add something before the festivities begin. In truth I’ve been pretty busy over the past few weeks. I spent a few days doing an excellent CTT+ course at Rezound in Sheffield plus I’ve also just started work on a new SharePoint 2010 book for McGraw-Hill. That’s not to mention my outstanding commitment to Wrox to deliver a short e-book on SharePoint 2007 Packaging and Deployment, my ongoing development work and the joys of having two children, one of which hasn’t quite grasped the concept of sleeping yet!

Anyways, I’ve been following a few posts on the MSDN forums about programmatically setting item level permissions when a document is added to a library. It’s a topic that probably needs a more in-depth explanation than can usually be provided in the forums so I thought I’d prepare a post on it (Plus I promised Winky42 I’d post something before the end of the week!)

So why would you want to do this?

Let’s say you have a document library where anybody can upload documents. Most likely you’ll achieve this by giving a particular user or group “Contribute” permissions for the site (or the document library if you’re not using inheritance). The “Contribute” role definition allows a user to view, add, update and delete items so they’ll have permissions to add documents to your library. However, they’ll also have permissions to view, edit or delete all the items in the document library. What if you want to allow users to upload documents but you don’t want them to be able to view documents added by other users?  Or what if you don’t want them to be able to delete other users documents?

The answer is item level permissions. In SharePoint a number of objects implement the ISecurableObject interface. This interface defines the methods and properties necessary for configuring security on an item. The interface is implemented by the SPWeb object, the SPList object and the SPListItem object, allowing distinct permissions to be attached to each. In order to build the functionality we need, we need to set unique permissions on the SPListItem object. Of course you could do this using the user interface but in a situation where documents are uploaded by many users, you’d need to manually set the permissions on each document which clearly isn’t practical. (Unless you’re planning on pursuing a compensation claim for RSI as a means of supplementing your pension income in later life.)

Fair enough, I can see where it would be useful. How do I do it?

Item level permissions can be set automatically using an event handler attached to the document library in question. The actual process of attaching the event handler to a list or library is dependent on how you’ve created the list. For example, if the list is created using a site definition or a feature, you’ll be able to attach a distinct ID to it and use this ID to automatically hook up the event handler. Whereas, if you’ve created the list using the user interface, you’ll have to write some code to programmatically attach the event handler. Suffice to say, attaching event handlers is a topic in it’s own right. (Here’s a good reference that explains how to do it using CAML and another that explains how to do it using code).

We’ll sidestep the issue so that we can focus on the matter at hand by using a tool to attach the event handler to the list using the user interface.  Thankfully, Chris White has produced just such a tool.

Another tool that You’ll need to follow this walkthrough is Carsten Keutmann’s WSPBuilder (‘cos life’s too short to do it any other way)

So now that we’ve got the tools out of the way, lets get stuck into the walkthough:

Setup the project

Create a new WSPBuilder project in Visual Studio 2008, I’ve called mine ItemSecurityDemo:

image

Once you’ve done that add a new Event Handler feature. Right click on the Project node, select Add > New Item. In the dialog that pops up select WSPBuilder > Event Handler. Call it SetItemLevelSecurity

image

If you’re feeling particularly conscientious you can populate the description in the feature settings dialog that appears. You can leave the scope as Web.

All being well you’ll end up with a project that looks remarkably like this:

image

Add some code

Now that we have an empty project we can start adding the code to make it do what we need. You’ll notice that WSPBuilder has added a folder called FeatureCode and that the folder contains a file called SetItemLevelSecuirty.cs (or something different if you didn’t use the feature name that I suggested).

If you open up this file you’ll see that it contains code similar to:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;

namespace ItemLevelSecurityDemo
{
    class SetItemLevelSecurity : SPItemEventReceiver
    {
        public override void ItemAdded(SPItemEventProperties properties)
        {
            base.ItemAdded(properties);
        }

        public override void ItemAdding(SPItemEventProperties properties)
        {
            base.ItemAdding(properties);
        }

        public override void ItemUpdated(SPItemEventProperties properties)
        {
            base.ItemUpdated(properties);
        }

        public override void ItemUpdating(SPItemEventProperties properties)
        {
            base.ItemUpdating(properties);
        }

    }
}

I’m sure you’ll agree that there’s not a lot of excitement in there. Since we’re only interested in setting permissions when an item is added we’ll add some extra code to the ItemAdded override.

public override void ItemAdded(SPItemEventProperties properties)
{
    //Get configuration from feature
    Guid featureGuid = new Guid("210e80f0-a432-4f51-9ced-e859e93f9e48");
    SPWeb web = properties.ListItem.Web;
    SPFeature feature = web.Features[featureGuid];
                
    Dictionary<string, string> principals;

    //get the name of the config file
    string filename = feature.Properties["ConfigFile"].Value;
    string configFile = Path.Combine(feature.Definition.RootDirectory, filename);

    //make sure that the file exists
    if (File.Exists(configFile))
    {
        principals = new Dictionary<string, string>();

        //load the configuration into an XMLReader
        using (XmlReader rdr = XmlReader.Create(File.OpenRead(configFile)))
        {
            //Parse the configuration into a Dictionary
            const string STR_Principal = "Principal";
            const string STR_Name = "name";
            const string STR_Level = "level";

            rdr.ReadToFollowing(STR_Principal);

            while (!rdr.EOF)
            {                    
                string name = rdr.GetAttribute(STR_Name);                        
                string level = rdr.GetAttribute(STR_Level);                        

                principals.Add(name, level);

                rdr.ReadToFollowing(STR_Principal);
            }
        }
        //pass the dictionary to the SetSecurty method to do the real work
        SetSecurity(properties, principals);
    }

    base.ItemAdded(properties);

}

There are a few things in this code that warrant explanation.

Firstly the featureGuid variable. If you go back to your project, go to 12 > FEATURES > SetItemLevelSecurity, you’ll see a file named feature.xml. This file defines our feature. If you open up the file it’ll look something like this:

image

I’ve highlighted the Id attribute, you need to cut and paste this value into the featureGuid variable. This Id uniquely identifies your feature and is used by the code to pick up a configuration file that we’ll add to the feature next.

Secondly, the config file. We’re writing code to set the permissions on an items as it’s created so we need some method of telling our code what permissions we want to set. Since there isn’t a user interface for this information, we’ll store it in an XML file that’ll be installed with our feature. We can then edit the xml file using a standard text editor if we need to make changes. So the first thing we need to do is add a new xml file to our feature.

Right click on the SetItemLevelSecurity node, select Add > New Item. In the dialog that appears select Data > XML File. Call the file SecuirtyConfig.xml

image

The SecurityConfig.xml file is pretty simple, it contains a list of principal names and the permissions that we’d like to give them for each new item:

<?xml version="1.0" encoding="utf-8" ?>
<Security>
  <Principal name="SecurityTest Owners" level="Read, Contribute" />
  <Principal name="SecurityTest Visitors" level="Full Control" />
  <Principal name="Any User Group" level="Custom Level" />
  <Principal name="Mydomain\username" level="Contribute" />
</Security>

The name attribute can contain either a SharePoint group name or a username. The level attribute contains a comma delimited list of the roles that you want to attach to the principal.

So now that we have a new configuration file in our feature, we need a way to tell the feature the name of the file. We can do this by adding a Property element to the feature.xml file. So your amended feature.xml file will look something like this:

<Feature  Id="210e80f0-a432-4f51-9ced-e859e93f9e48"
          Title="ItemLevelSecurity Feature"
          Description="Description for SetItemLevelSecurity"
          Version="12.0.0.0"
          Hidden="FALSE"
          Scope="Web"
          DefaultResourceFile="core"
          xmlns="http://schemas.microsoft.com/sharepoint/">
  <Properties>
    <Property Key="ConfigFile" Value="SecurityConfig.xml"/>
  </Properties>
  <ElementManifests>
    <ElementManifest Location="elements.xml"/>
  </ElementManifests>
</Feature>

Now if we go back to our code, we can quickly review what it’s doing:

  1. It gets the name of the config file from the property we’ve just added to our feature
  2. It loads the config file (if it exists)
  3. The file is parsed into a Dictionary of strings containing the information required to setup permissions on our item
  4. The Dictionary is passed to the SetSecurity method for processing

The Crux of the matter

Now that we have an event handler that will load up the configuration when an item is added and pass it to a SetSecurity method for processing, the next logical step is to implement the SetSecurity method.

To your SetItemSecurity.cs file add the following code:

private void SetSecurity(SPItemEventProperties properties, Dictionary<string, string> principals)
{
    //Use site owner account instead of running everything in an elevated privileges delegate. 
    //it's easier to debug

    SPUserToken sysToken = null;
    //get the system account security token
    SPSecurity.RunWithElevatedPrivileges(delegate()
    {
        using (SPSite current = new SPSite(properties.SiteId))
        {
            sysToken = current.SystemAccount.UserToken;
        }
    });

    //use the token to create a new SPSite running in the context of the system account
    using (SPSite site = new SPSite(properties.SiteId, sysToken))
    {
        //We're effectively logged in as system account while running this code
        using (SPWeb web = site.OpenWeb(properties.ListItem.Web.ID))
        {
            //get a reference to our list
            SPList myList = web.Lists[properties.ListId];

            //get a reference to our new item
            SPListItem item = myList.Items.GetItemById(properties.ListItemId);

            bool allowUnsafe = web.AllowUnsafeUpdates;

            try
            {
                web.AllowUnsafeUpdates = true;

                //break inheritance if it hasn't been done already
                if (!item.HasUniqueRoleAssignments)
                {
                    item.BreakRoleInheritance(false);
                    web.AllowUnsafeUpdates = true;
                    // BreakRoleInheritance causes AllowUnsafeUpdates to reset to it's default setting.
                }

                //If we have any principles in our config file
                if (principals != null && principals.Count > 0)
                {
                    //loop through them
                    foreach (string key in principals.Keys)
                    {
                        //levels are stored as comma delimited
                        string[] levels = principals[key].Split(',');

                        foreach (string level in levels)
                        {
                            SPRoleDefinition roleDef;

                            //get a reference to the role definition that we've configured
                            try
                            {
                                roleDef = web.RoleDefinitions[level.Trim()];
                            }
                            catch (Exception)
                            {
                                //If we can't find the role definition then we can't apply it!
                                Trace.WriteLine(string.Format("Unable to set item security. Role Definition [{0}] could not be found", level));

                                //If you're using MOSS, uncomment this line to write to the MOSS logs
                                //PortalLog.DebugLogString(PortalLogLevel.Unexpected, "Unable to set item security. Role Definition [{0}] could not be found.", level);
                                break; //move onto the next principle
                            }
                            
                            SPRoleAssignment assignment = null;

                            //get a reference to the securty principal that we want to grant permissions to
                            SPPrincipal principal = GetPrincipal(web, key);

                            if (principal != null)
                            {
                                assignment = new SPRoleAssignment(principal);

                                if (assignment != null)
                                {
                                    //bind the permissions to the principal
                                    assignment.RoleDefinitionBindings.Add(roleDef);
                                    item.RoleAssignments.Add(assignment);
                                }
                            }
                        }
                    }

                    //update out item to save the new permissions
                    item.Update();
                }
            }
            catch (Exception ex)
            {
                throw new SPException("Error setting security for item.", ex);
            }
            finally
            {
                if (allowUnsafe)
                {
                    // Ensure status is set to what it was before we changed it.
                    web.AllowUnsafeUpdates = allowUnsafe;
                }
            }
        }
    }

}

private static SPPrincipal GetPrincipal(SPWeb web, string key)
{
    //Principals can be either users or groups (both derive from SPPrincipal)

    SPPrincipal principal=null;

    //if the principal name contains a backslash, assume it's a username (in the format domain\username)
    if (key.Contains(@"\"))
    {
        try
        {
            principal = web.SiteUsers[key];
        }
        catch (SPException)
        {
            Trace.WriteLine(string.Format("Unable to set item security. User [{0}] could not be found.", key));

            //If you're using MOSS, uncomment this line to write to the MOSS logs
            //PortalLog.DebugLogString(PortalLogLevel.Unexpected, "Unable to set item security. User [{0}] could not be found.", key);
        }
    }
    else
    {
        //otherwise assume the principal is a group
        try
        {
            principal = web.SiteGroups[key];
        }
        catch (SPException)
        {
            Trace.WriteLine(string.Format("Unable to set item security. Group [{0}] could not be found.", key));

            //If you're using MOSS, uncomment this line to write to the MOSS logs
            //PortalLog.DebugLogString(PortalLogLevel.Unexpected, "Unable to set item security. Group [{0}] could not be found.", key);
        }
    }

    return principal;
}
 
This code is pretty self explanatory I think. Maybe the only thing that needs clarification is the SPSecurity.RunWithElevatedPrivileges bit. As you can imagine changing item permissions is a security sensitive operation. Not all users will have the necessary permissions.

We need to make sure that our event handler code has sufficient privileges to change the security on an item and we can do this by using the SPSecuirty.RunWithElevatedPrivileges function. The only problem with this function is that you can’t get the debugger to stop inside the delegate and given that this is a walkthrough and you’ll probably want to mess around with this code, particularly the stuff that’s running with elevated privileges. I’ve done a bit more work in there to make it easier to debug. For more info on SPSecuirty.RunWithElevatedPrivileges check out Mark Wagner’s article for a bit more info.

So we now have all the code required to set permissions on an item using an event handler. The next thing we need to do is attach the event handler to a document library. As I mentioned earlier there are a few ways to do this and if you look at your project in the 12 > TEMPLATE > FEATURES > SetItemLevelSecurity > elements.xml you’ll see one example of how it can be done. We won’t get into that in detail here though, I just mention it in case you wondered what that extra file was for! We’ll make use of Chris White’s tool for simplicity’s sake.

Before we attach the event handler we need to install it, this is where WSPBuilder does it’s stuff. In Visual Studio, on the Tools menu, select WSPBuilder > Build WSP. Once that’s done, select Tools > WSPBuilder > Deploy. You’re code should now be installed and ready for use.

In the user interface, if you select your document library,go to Settings > Document Library Settings. You’ll see there’s a new menu item in General Settings called Event Receivers (If you can’t see this menu then the tool isn’t installed properly, check out Chris’ site for more info)

image

Clicking on this new item will allow you to add your new event handler to this document library. On the Event Receivers page select New. In the Add Event Receiver page that follows enter the following information:

image

“Wait a minute”, I hear you say, where do i get the public key etc. for the assembly name field? Remember the elements.xml file that I mentioned earlier as one possible way of adding an event handler? If you open that up it’ll look a bit like this:

 

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Receivers ListTemplateId="100">
    <Receiver>
      <Name>AddingEventHandler</Name>
      <Type>ItemAdding</Type>
      <SequenceNumber>10000</SequenceNumber>
      <Assembly>ItemLevelSecurityDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=26469dab22364b25</Assembly>
      <Class>ItemLevelSecurityDemo.SetItemLevelSecurity</Class>
      <Data></Data>
      <Filter></Filter>
    </Receiver>
  </Receivers>
</Elements>

You’ll see that the Assembly and Class elements contain everything that you need. Simply cut and paste these values into the page, click Ok and you’re done. Now whenever you add documents to your library, you event handler will set the permissions as configured in your secuirtyconfig.xml file.

Conclusion

We’ve added a custom event handler that will setup the permissions on an item as soon as it’s added to a document library or list. The permissions are configurable using an XML file that’s deployed along with our feature. This project is meant purely as an example of how to setup item level permissions programmatically rather than as the single best solution.For example, although the permissions are configurable via the XML file, a better solution if this type of code was to be applied to many document libraries each with different permissions, would be to implement an information management policy with a user interface for configuring permissions. The policy could then be easily reused and reconfigured using the SharePoint user Interface. Maybe that’d be an interesting CodePlex project for somebody with more free time than me?

I hope this information is useful. If there’s anything that I haven’t covered or if you have any problems with this code, please feel free to post a comment.