Thursday, December 24, 2009

Custom Auditing In SharePoint

Code download available at: OfficeSpace2008_09a.exe (631 KB)

Browse the Code Online

 Contents

The AuditingDemo Project

Creating a Page to Support Auditing Configuration

Extending the Site Actions Menu

Viewing Audit Log Entries

Viewing Logs on a Per-Item Basis

Many people using SharePoint® technologies don't realize that there is auditing support built directly into the Windows® SharePoint Services (WSS) 3.0 platform. The primary reason this is not well-known is that WSS auditing support is turned off by default, and it cannot be enabled using anything supplied by WSS out of the box. Taking advantage of the WSS auditing infrastructure requires extra code that is not included with WSS.

One simple way to take advantage of WSS auditing support is to use Microsoft® Office SharePoint Server (MOSS) 2007. MOSS provides a user interface (under Site Settings > Configure Audit Settings) that allows a site-collection owner to enable and configure automatic activity event logging. MOSS also provides an audit-reporting facility that reads audit log entries and reports on which users have been engaging in activities, such as reading and writing content, within a specific site collection.

A second approach to using WSS auditing support will appeal to you as a developer. You can build a custom SharePoint solution with Visual Studio® and create a custom application page that allows a site-collection administrator to enable and configure WSS activity event logging. But it's not enough just to turn on automatic activity event logging. It requires more than that. A custom auditing solution targeting WSS must also include some type of user interface so that users can view or report on audit entries that have been added to the WSS audit log.

Two years ago, not long after Microsoft released the first public beta for MOSS 2007, Joanna Bichsel and I published a white paper titled "Item-Level Auditing with SharePoint Server 2007" (msdn.microsoft.com/library/bb397403), along with an accompanying code sample. In this white paper, we discussed many important details about how to program auditing support in WSS. We also covered how MOSS leverages and extends the WSS auditing infrastructure using custom policies and audit report generation.

In this month's column I am going to walk through a new SharePoint solution I recently created with a Visual Studio project named AuditingDemo. The project contains some of the same auditing management code I covered in the white paper, such as the code that enables auditing support through the WSS object model. However, the AuditingDemo project has been totally redesigned and is a much better starting point than the sample code I wrote two years ago. For example, the AuditingDemo project has been created using the STSDEV utility, and I have incorporated many best practices in areas such as solution-package deployment, security programming, and security trimming.

My goal with this month's column is to explain these updated best practices and SharePoint development techniques. It is my intention to complement the original white paper rather than overlap its content. If you have not read the original white paper, I recommend taking a look at it first so that you have a proper context for this month's column.


 

The AuditingDemo Project

The purpose of the AuditingDemo project is to provide a custom SharePoint solution that can take advantage of the auditing support built into the WSS platform. I designed the project around a scenario wherein a site-collection owner (or another user in the role of Audit Manager) can enable, view, and modify audit settings for the current site collection. The AuditingDemo project also provides viewing capabilities so that a user in the role of an auditor can see what's in the audit log to determine what activity has occurred on a site-collection-wide basis and also on a per-item or per-document basis.

As I mentioned earlier, this project was built using the STSDEV utility, which you can download from codeplex.com/stsdev. You can read about it in my March 2008 Office Space column (msdn.microsoft.com/magazine/cc337895).

The AuditingDemo project has been designed with a central feature (also named AuditingDemo) that is scoped to the level of the site collection. To use the AuditingDemo, you simply need to activate the feature once per site collection.

The AuditingDemo feature includes a feature receiver class with a FeatureActivated event handler (see Figure 1). The code inside FeatureActivated executes during feature activation, and it accomplishes two important things. First, it fully enables audit logging for the entire site collection. Second, it creates a few security objects specific to the AuditingDemo solution. In particular, the code inside FeatureActivated creates two new SharePoint groups and two new permission levels that allow the site-collection administrator to add users into the roles of Auditor and Audit Manager.

 Figure 1 The FeatureActivated Event Handler


Copy Code

public override void FeatureActivated(SPFeatureReceiverProperties

properties) {

using (SPSite siteCollection = (SPSite)properties.Feature.Parent) {

SPWeb TopLevelSite = siteCollection.RootWeb;


 

// Turn on auditing flags.

siteCollection.Audit.AuditFlags = SPAuditMaskType.All;

siteCollection.Audit.Update();


 

// create permission levels

SPRoleDefinition AuditorPermissions, AuditManagerPermissions;

AuditorPermissions = CreatePermissionLevel(

"Auditor Permissions",

"Can view audit logs",

"Read");

AuditManagerPermissions = CreatePermissionLevel(

"Audit Manager Permissions",

"Can configure auditing support",

"Design",

SPBasePermissions.ManageWeb);


 

// create Auditors group

SPGroup Auditors = CreateGroup(

"Auditors",

"for users who need to audit user activity");

SPRoleAssignment AuditorRoleAssignment = new SPRoleAssignment(Auditors);

AuditorRoleAssignment.RoleDefinitionBindings.Add(AuditorPermissions);

TopLevelSite.RoleAssignments.Add(AuditorRoleAssignment);


 

// create Audit Managers group

SPGroup AuditManagers = CreateGroup(

"Audit Managers",

"for users who configure WSS auditing support");

SPRoleAssignment AuditManagerRoleAssignment =

new SPRoleAssignment(AuditManagers);

AuditManagerRoleAssignment.RoleDefinitionBindings.Add(

AuditManagerPermissions);

TopLevelSite.RoleAssignments.Add(AuditManagersRoleAssignment);

}

Note that the code in the FeatureActivated event handler calls the two utility methods named CreatePermissionLevel and CreateGroup. The reason I wrote these was to ensure that any preexisting security objects with the same name as those being created were first deleted. Each method then creates the requested security object (permission level or group), adds it to the appropriate collection within the top-level site, and returns a strongly typed security object to the caller. The CreatePermissionLevel method returns an SPRoleDefinition object that represents the new permission level, and the CreateGroup method returns an SPGroup.

Note that I designed the CreatePermissionLevel method with several overloaded implementations that make the third and fourth parameters optional. The third parameter is for passing the name of an existing permission level that is used to make a copy for the new permission level. For example, the AuditorPermissions permission level is created by making a copy of the built-in Read permission level. And the AuditManagerPermissions permission level is created by making a copy of the built-in Design permission level.

The fourth parameter to the CreatePermissionLevel method is of the SPBasePermissions type, which is used to pass one or more extra permissions that are then used to initialize the new permission level. For example, the call to CreatePermissionLevel that creates the SPRoleDefinition object AuditManagerPermissions passes a value of ManageWeb for the fourth parameter. This means that the new permission level is created with the same permissions as the built-in Design permission level plus the ManageWeb permission.

The ManageWeb permission is important because it's required for users who need to adjust audit settings. I will cover two security trimming techniques later in this column that can be used to display menu items to just those users that have this permission.

The CreateGroup method does a little more than add a new group to the top-level site. It also adds the new group to the associated group's collection and adds the ID of the new group into a site-level property named vti_associatemembergroup. This extra code is important if you want the new group to show up in the group's QuickLaunch bar and inside the combobox of the Add Users page, which makes it far more intuitive for the site-collection owner to add users to the new group.


 

Creating a Page to Support Auditing Configuration

The AuditingDemo project provides three custom app pages—AuditConfig.aspx, AuditLogViewer.aspx, and ItemAudit.aspx—that were developed using best-practice techniques. For example, these application pages are deployed within a solution-specific directory named AuditingDemo inside the LAYOUTS directory. The code that provides the behavior for these pages has been written in codebehind files (such as AuditConfig.cs). The codebehind files are then compiled into the project's main assembly, AuditingDemo.dll, which is installed and loaded from the Global Assembly Cache (GAC).

The AuditConfig.aspx application page provides a user interface for configuring WSS auditing support. I added a CustomAction to the AuditingDemo feature so that users with the ManageWeb permission will be presented with a link on a site-settings page that allows them to navigate to AuditConfig.aspx:


Copy Code

<!-- Add Link to Site Setting Page -->

<CustomAction

Id="SiteActionsToolbar"

GroupId="SiteCollectionAdmin"

Location="Microsoft.SharePoint.SiteSettings"

Sequence="0"

Rights="ManageWebs"

Title="Audit Management" >

<UrlAction Url="~sitecollection/_layouts/AuditingDemo/AuditConfig.aspx"/>

</CustomAction>

Note that I created this custom action with a GroupId attribute value of SiteCollectionAdmin and a Sequence attribute value of 0. This is what makes the link show up first in the Site Collection Administration column of the Site Settings page.

The Rights attribute, which I have included with a value of ManageWebs, is used by WSS to provide declarative security trimming. Therefore, only users who have the ManageWebs permission will see this link. This will be the case for site-collection owners as well as any users that the site-collection owner has added to the role of Audit Manager.

When users navigate to AuditConfig.aspx, they see the UI in Figure 2. As you can see, this application page provides radio buttons to fully enable or disable auditing. You will also find a radio button for entering a selective mode where the user can choose exactly what activities are recorded with an audit log entry.



Figure 2 AuditConfig.aspx Lets Users Configure Audit Logging

The code that is behind the OK button of AuditConfig.aspx is pretty simple. It reads which radio button has been selected by the user and sets the audit flags for the current site collection to the appropriate value. Then it calls Update on the Audit property to record the changes into the Content Database:


Copy Code

SPSite siteCollection = this.Site;

if (radAuditingOff.Checked) {

siteCollection.Audit.AuditFlags = SPAuditMaskType.None;

}

if (radAuditingOnFull.Checked) {

siteCollection.Audit.AuditFlags = SPAuditMaskType.All;

}

if (radAuditingOnSelective.Checked) {

siteCollection.Audit.AuditFlags = GetSelectiveAuditingFlags();

}

siteCollection.Audit.Update();

If the user has chosen the option for selective auditing configuration, there is a call to the GetSelectiveAuditingFlags utility method. It inspects all those checkboxes and uses bitwise OR operations to return an SPAuditTypeMask value that represents a combination of all the types of activities that the user has selected for auditing:


Copy Code

private SPAuditMaskType GetSelectiveAuditingFlags() {

SPAuditMaskType AuditFlags = SPAuditMaskType.None;

if (chkAuditView.Checked) {

AuditFlags |= SPAuditMaskType.View;

}

if (chkAuditUpdate.Checked) {

AuditFlags |= SPAuditMaskType.Update;

}

// Repeat for Copy, Move, Delete, Undelete, CheckIn, CheckOut,

// Search, Workflow, SecurityChange, ProfileChange, SchemaChange

return AuditFlags;

}


 

Extending the Site Actions Menu

I've provided navigation support so that users can get to the AuditLogViewer.aspx application page. This is a page designed for users in the role of auditor who want to see what activity has occurred on the site. I decided against putting another link on the Site Settings page because many of the users in the role of auditor may not necessarily be administrators and therefore may never go to the Site Settings page for any other reason. Therefore, adding navigation support using the custom Site Actions menu items seemed more intuitive. It also gives me a chance to show you how to create a flyout menu with programmatic security trimming.

When you want to build a menu item dynamically, you need to create a custom control class and also add a declarative CustomAction element to instantiate an instance of the control class in the correct place. Note how the following CustomAction element differs from the one I showed earlier:


Copy Code

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

<!-- Add Command to Site Actions Dropdown -->

<CustomAction Id="AuditingSupport"

GroupId="SiteActions"

Location="Microsoft.SharePoint.StandardMenu"

Sequence="1000"

ControlClass="AuditingDemo.SiteActionsCustomSubMenu"

ControlAssembly="AuditingDemo, [4-part name]"

Title="Auditing Support"

Description="Support for configuring and viewing auditing" />

</Elements>

The main difference with this CustomAction element is that it has been declared using a ControlClass attribute and a ControlAssembly attribute instead of a UrlAction element. This makes it possible to generate menu items dynamically using a custom control class such as the SiteActionsCustomSubMenu, which is used in the AuditingDemo project and is shown in the code in Figure 3. Note that a custom control class must inherit from the WebControl class supplied by the ASP.NET programming model.

 Figure 3 SiteActionsCustomSubMenu


Copy Code

public class SiteActionsCustomSubMenu : WebControl {


 

protected override void OnLoad(EventArgs e) {

this.EnsureChildControls();

base.OnLoad(e);

}


 

protected override void CreateChildControls() {

SPSite siteCollection = SPContext.Current.Site;

SPWeb site = SPContext.Current.Web;

SPUser user = site.CurrentUser;

// provide security trimming

if (IsCurrentUserInGroup("Auditors") ||

IsCurrentUserInGroup("Audit Managers") ||

user.IsSiteAdmin) {

string siteCollectionPath = siteCollection.Url;

if (!siteCollectionPath.EndsWith(@"/"))

siteCollectionPath += "/";


 

SubMenuTemplate smt = new SubMenuTemplate();

smt.Text = "Auditing Support";

smt.ID = "mnuAuditingSupport";

smt.Description = "Configure and View Auditing";

smt.ImageUrl = siteCollectionPath +

@"_layouts/images/AuditingDemo/AfricanPith32.gif";

smt.Sequence = 400;


 

MenuItemTemplate mit1 = new MenuItemTemplate();

mit1.ID = "mnuAuditLog";

mit1.Text = "Audit Log";

mit1.Description = "Inspect Audit Entries";

mit1.Sequence = 401;

mit1.ClientOnClickNavigateUrl = siteCollectionPath +

"_layouts/AuditingDemo/AuditLogViewer.aspx";

mit1.ImageUrl = siteCollectionPath +

@"_layouts/images/AuditingDemo/Binoculars32.gif";

// add menu item to Controls collection

smt.Controls.Add(mit1);

// perform extra security trimming for menu for AuditConfig.aspx

if (IsCurrentUserInGroup("Audit Managers") || user.IsSiteAdmin) {

MenuItemTemplate mit2 = new MenuItemTemplate();

mit2.ID = "mnuAuditingConfiguration";

mit2.Text = "Auditing Configuration";

mit2.Description = "Enable/Disable Auditing";

mit2.Sequence = 402;

mit2.ClientOnClickNavigateUrl = siteCollectionPath +

"_layouts/AuditingDemo/AuditConfig.aspx";

mit2.ImageUrl = siteCollectionPath +

@"_layouts/images/AuditingDemo/Compass32.gif";

smt.Controls.Add(mit2);

}


 

this.Controls.Add(smt);

}

}


 

private bool IsCurrentUserInGroup(string GroupName) {

SPWeb site = SPContext.Current.Web;

foreach (SPGroup group in site.SiteGroups) {

if (group.Name.Equals(GroupName)) {

return group.ContainsCurrentUser;

}

}

throw new ApplicationException("There is no group named " +

GroupName);

}

}

The CreateChildControls method provides dynamic security trimming. Note that the entire flyout menu is only shown to users who are either site-collection owners or who have been added to the Auditors or Audit Manager groups. The second menu item, which allows a user to navigate to the AuditConfig.aspx page, is further trimmed to exclude users in the Auditors group. This menu is only displayed to site-collection owners or users who have been added to the Audit Manager group.

There is one more important point I want to make about creating a dynamic menu with a custom control class. Like a Web Part, a custom control class requires a SafeControl entry in the web.config file of the hosting Web application. Here, the STSDEV utility provides the support for adding this SafeControl. It adds the appropriate elements into the manifest.xml file, and, as a result, the required SafeControl entry is automatically added to one or more web.config files whenever the AuditingDemo.wsp solution package is deployed within a farm.


 

Viewing Audit Log Entries

The AuditingDemo solution provides the AuditLogViewer.aspx custom application page shown in Figure 4. This page provides the users with a view of all the audit entries for the entire site collection. There is also a button on this page with the caption Clear Audit Log. This button is security trimmed so it is only shown to site-collection owners. The code behind this button deletes all entries from the audit log, which you might find to be helpful while testing a custom auditing solution.



Figure 4 AuditLogViewer.aspx Displays Audit Log Entries

The code behind AuditLogViewer.aspx uses an SPAuditQuery object to query the audit log of the current site collection. A query is run by calling the GetEntries method on the Audit property of an SPSite object. A call to GetEntries returns an SPAuditEntryCollection object that can be enumerated to inspect each audit entry as an SPAuditEntry object:


Copy Code

SPSite SiteCollection = SPContext.Current.Site;

SPAuditQuery wssQuery = new SPAuditQuery(SiteCollection);

SPAuditEntryCollection auditCol =

SiteCollection.Audit.GetEntries(wssQuery);

foreach (SPAuditEntry entry in auditCol) {

// enumererate through each audit entry

}

There are many ways in which you can enumerate through an SPAuditEntryCollection to create a display. I used a technique where I dynamically create an ADO.NET DataTable object and then populate it with one row of information per audit entry. By creating a DataTable, it becomes relatively simple to display the audit information by binding it to an SPGridView control.

The code behind the AuditLogViewer.aspx page must deal with a special security concern. If you examine the code that queries the audit log, you will see that it first makes a call to RunWithElevatedPrivileges so that it can run under the identity of the privileged SHAREPOINT\System account. Using this technique is required because querying the audit log is a privileged operation.

There will likely be scenarios where a user is added into the role of Auditor, but this user will not be granted the permissions that are necessary in order to query the audit log. Therefore, a call to RunWithElevatedPrivileges allows your code to query the audit log, even if the current user does not have the required permissions to do so.


 

Viewing Logs on a Per-Item Basis

The third application page in the AuditingDemo project is ItemAudit.aspx. This page shows audit entries for one particular item or document. Navigation to this page is provided by adding a CustomAction element that adds a new EditControlBlock (ECB) menu item to the built-in Item content type:


Copy Code

<CustomAction Id="ItemAuditing.ECBItemMenu"

RegistrationType="ContentType"

RegistrationId="0x01"

ImageUrl="/_layouts/images/GORTL.GIF"

Location="EditControlBlock"

Sequence="300"

Title="View Audit History">

<UrlAction Url=

"~site/_layouts/AuditingDemo/ItemAudit.aspx?ItemId=

{ItemId}&amp;ListId={ListId}"/>

</CustomAction>

A CustomAction configured for the Item content type will also apply to any content types that inherit from Item. And since all content types inherit from the Item content type, this technique will effectively add an ECB menu item to every item and document across the entire site collection.

You can test this technique out by downloading the AuditingDemo sample code and then deploying it within the farm on a SharePoint development machine. Once you have completed activation of the AuditingDemo feature in a particular site collection, you will see that every item and document now has an ECB menu item with a View Audit History title. Whenever a user selects that ECB menu item, he is redirected to the ItemAudit.aspx page, as shown in Figure 5.



Figure 5 ItemAudit.aspx Displays Audit Entries for a Particular Item

Like the AuditLogViewer.aspx page, the code behind ItemAudit.aspx uses an SPAuditQuery object to retrieve audit log data as well as a DataTable that is populated with audit log entry data and then bound to an SPGridView control. The main difference is that the code behind ItemAudit.aspx makes a call to the RestrictToListItem method of the SPAuditQuery object before running the query to filter the results so that they only pertain to the item or document in question.

It's important to note that the SPAuditQuery class also provides two additional filtering methods, RestrictToList and RestrictToUser, so that you can see audit entries for a particular list or a particular user. Also note that you can use the RestrictToUser together with either the RestrictToListItem method or the RestrictToList method to determine what a specific user has been doing with either a list or a list item.


 

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, and has just completed his book Inside Windows SharePoint Services 3.0 for Microsoft Press. He also delivers advanced SharePoint training to professional developers through his company, Ted Pattison Group (www.TedPattison.net).


 

0 comments: