MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Friday, November 30, 2007

Set Audit Settings for Site Collection

In any financial institution auditing is crucial - it's no different with my company - as such we wanted to make sure that there was at least a minimum level of auditing occurring at the site collection level. From the site collection settings page you can get to the "Site collection audit settings" page where some basic auditing can be enabled. For more complex stuff you can setup custom policies and associate them at various levels. However, for our initial deployment we wanted to at least have some of these basic settings enabled for every site collection. To automate these settings during our upgrade I created a new command: gl-setauditsettings. This command turned out to be really easy to create and only took me a few minutes. Only thing I stumbled on was figuring out the best way to handle replacing, adding, or removing settings so that I didn't have to create more than one command. In the end I opted for a simple mode parameter which enables you to state your intent - each setting is then a simple parameter that's passed in. The code, shown below, gets the SPAudit object via the SPSite's Audit property and then sets the AuditFlags property appropriately:

   1: public override int Run(string command, StringDictionary keyValues, out string output)
   2: {
   3:  output = string.Empty;
   4:  
   5:  InitParameters(keyValues);
   6:  
   7:  string url = Params["url"].Value.TrimEnd('/');
   8:  ModeEnum mode = (ModeEnum) Enum.Parse(typeof (ModeEnum), Params["mode"].Value, true);
   9:  
  10:  using (SPSite site = new SPSite(url))
  11:  {
  12:   // Initialize the mask to it's default.
  13:   SPAuditMaskType auditMask = SPAuditMaskType.None;
  14:   if (mode != ModeEnum.Replace)
  15:    auditMask = site.Audit.AuditFlags; // We're not replacing the mask so we need to store the current settings.
  16:  
  17:   if (mode == ModeEnum.Remove)
  18:   {
  19:    // Remove settings
  20:    foreach (SPAuditMaskType mask in Enum.GetValues(typeof(SPAuditMaskType)))
  21:    {
  22:     if (Params[mask.ToString()].UserTypedIn)
  23:      auditMask = auditMask & ~mask;
  24:    }
  25:   }
  26:   else
  27:   {
  28:    // Add settings (replace is just an add but starts with a blank slate)
  29:    foreach (SPAuditMaskType mask in Enum.GetValues(typeof(SPAuditMaskType)))
  30:    {
  31:     if (Params[mask.ToString()].UserTypedIn)
  32:      auditMask = auditMask | mask;
  33:    }
  34:   }
  35:   // Update the Audit object with the new flags
  36:   site.Audit.AuditFlags = auditMask;
  37:   site.Audit.Update();
  38:  }
  39:  
  40:  return 1;
  41: }

The syntax of the command can be seen below:

C:\>stsadm -help gl-setauditsettings

stsadm -o gl-setauditsettings

Set the events that should be audited for documents, items, lists, libraries, and sites within the site collection.

Parameters:
        -url <site collection url>
        -mode <replace | add | remove>
        [-none]
        [-checkout]
        [-checkin]
        [-view]
        [-delete]
        [-update]
        [-profilechange]
        [-childdelete]
        [-schemachange]
        [-securitychange]
        [-undelete]
        [-workflow]
        [-copy]
        [-move]
        [-search]
        [-all]
Here's an example of how to enable auditing of the delete and undelete events in addition to any existing events already monitored:
stsadm -o gl-setauditsettings -url "http://intranet" -mode add -delete -undelete
One thing to be aware of - when you edit these settings via the browser you are, in some circumstances, editing more than one setting at a time. For example, via the browser you cannot choose to audit delete events and not undelete events - they are combined into one setting. Using this command allows you to set the audit settings at a finer level so you can track just delete events without tracking undelete (in most cases you'll want to track both but it's nice to know that you can now treat them separately). Note however that if you use this command to enable just delete and not undelete the browser will show the check box for "Deleting or restoring items" as checked as it does an or comparison when enabling the check box.

Thursday, November 29, 2007

Search Scopes and Rules

Our previous environment had just one web application and no existing search scopes beyond the default ones. With our upgrade we wanted to (finally) take advantage of search scopes to help filter the result sets and make searches more relevant. In order to make the creation of scopes scriptable I needed three new commands: gl-createsearchscope, gl-updatesearchscope, and gl-addsearchrule. I thought about creating commands to support editing and deleting but as I don't currently have the need for that I decided against it (with the exception of the gl-updatesearchscope command which I needed to be able to assign my shared search scope to groups on the various web applications). For some reason I was expecting this to be more difficult than it was but after digging into it I found it to be rather easy. The commands I created are detailed below.

1. gl-createsearchscope

The code to work with search scopes is really straight forward. You obtain a "Microsoft.Office.Server.Search.Administration.Scopes" object which is effectively your scope manager object. From this you use the AllScopes property (which is a ScopeCollection object) and call the Create method passing in appropriate parameters. Once you've got your scope created you can add it to relavent groups by getting the ScopeDisplayGroup object via the GetDisplayGroup() method of the Scopes object. Note that the scope can be owned by a site collection or the SSP. If a null value is passed into the Create method for the owningSiteUrl parameter then the scope will be owned by the SSP (it will be a shared scope available to all site collections belonging to the SSP which is determined by the passed in url parameter which loads the appropriate SPSite object). The core code is shown below:

   1: public override int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
   2: {
   3:  output = string.Empty;
   4:  
   5:  InitParameters(keyValues);
   6:  
   7:  string url = Params["url"].Value.TrimEnd('/');
   8:  string name = Params["name"].Value;
   9:  string description = Params["description"].Value;
  10:  string searchPage = null;
  11:  if (Params["searchpage"].UserTypedIn)
  12:   searchPage = Params["searchpage"].Value;
  13:  bool sspIsOwner = Params["sspisowner"].UserTypedIn;
  14:  
  15:  using (SPSite site = new SPSite(url))
  16:  {
  17:   Scopes scopeManager = new Scopes(SearchContext.GetContext(site));
  18:  
  19:   // Create the scope
  20:   Scope scope = scopeManager.AllScopes.Create(name, description, (sspIsOwner ? null : new Uri(site.Url)), true,
  21:            searchPage, ScopeCompilationType.AlwaysCompile);
  22:  
  23:   // If the user passed in any groups then add the scope to those groups.
  24:   if (Params["groups"].UserTypedIn)
  25:   {
  26:    foreach (string g in Params["groups"].Value.Split(','))
  27:    {
  28:     ScopeDisplayGroup group;
  29:     try
  30:     {
  31:      group = scopeManager.GetDisplayGroup(new Uri(site.Url), g.Trim());
  32:     }
  33:     catch (Exception)
  34:     {
  35:      group = null;
  36:     }
  37:     if (group == null)
  38:     {
  39:      scope.Delete(); // We don't want the scope created if there was an error with the groups so undo what we've done.
  40:      throw new SPException(string.Format("Display group '{0}' not found.", g));
  41:     }
  42:     group.Add(scope);
  43:     group.Update();
  44:    }
  45:   }
  46:  }
  47:  
  48:  return 1;
  49: }

The syntax of the command can be seen below:

C:\>stsadm -help gl-createsearchscope

stsadm -o gl-createsearchscope

Sets the search scope for a given site collection.

Parameters:
        -url <site collection url>
        -name <scope name>
        [-description <scope description>]
        [-groups <display groups (comma separate multiple groups)>]
        [-searchpage <specific search results page to send users to for results when they search in this scope>]
        [-sspisowner]

Here's an example of how to create a shared search scope (owned by the SSP):

stsadm –o gl-createsearchscope -url "http://sspadmin/ssp/admin" -name "Search Scope 1" -description "A really helpful search scope." -groups "search dropdown, advanced search" -sspisowner

Note that the group assignments will not show up on other web applications - you must use the updatesearchscope command to associate the scope with groups on each web application of interest.

2. gl-updatesearchscope

This code is almost identical to that of the gl-createsearchscope command - the main difference is that I'm updating individual properties rather than calling the Create method and I have to clear out existing groups before adding the newly assigned groups:

   1: public override int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
   2: {
   3:  output = string.Empty;
   4:  
   5:  InitParameters(keyValues);
   6:  
   7:  string url = Params["url"].Value.TrimEnd('/');
   8:  string name = Params["name"].Value;
   9:  string description = Params["description"].Value + string.Empty;
  10:  string searchPage = null;
  11:  if (Params["searchpage"].UserTypedIn)
  12:   searchPage = Params["searchpage"].Value;
  13:  
  14:  using (SPSite site = new SPSite(url))
  15:  using (SPWeb web = site.RootWeb)
  16:  {
  17:   if (!web.CurrentUser.IsSiteAdmin)
  18:    throw new UnauthorizedAccessException();
  19:  
  20:   Scopes scopeManager = new Scopes(SearchContext.GetContext(site));
  21:  
  22:   Scope scope;
  23:   try
  24:   {
  25:    scope = scopeManager.GetScope(new Uri(site.Url), name);
  26:   }
  27:   catch (ScopeNotFoundException)
  28:   {
  29:    scope = scopeManager.GetScope(null, name);
  30:   }
  31:   if (Params["description"].UserTypedIn)
  32:    scope.Description = description;
  33:   if (Params["searchpage"].UserTypedIn)
  34:    scope.AlternateResultsPage = searchPage;
  35:  
  36:   scope.Update();
  37:  
  38:   // If the user passed in any groups then add the scope to those groups.
  39:   if (Params["groups"].UserTypedIn)
  40:   {
  41:    // Clear out any group settings.
  42:    foreach (ScopeDisplayGroup g in scopeManager.AllDisplayGroups)
  43:    {
  44:     if (g.Contains(scope))
  45:     {
  46:      g.Remove(scope);
  47:      g.Update();
  48:     }
  49:    }
  50:  
  51:    // Add back the specified groups.
  52:    foreach (string g in Params["groups"].Value.Split(','))
  53:    {
  54:     ScopeDisplayGroup group;
  55:     try
  56:     {
  57:      group = scopeManager.GetDisplayGroup(new Uri(site.Url), g.Trim());
  58:     }
  59:     catch (Exception)
  60:     {
  61:      group = null;
  62:     }
  63:     if (group == null)
  64:     {
  65:      scope.Delete(); // We don't want the scope created if there was an error with the groups so undo what we've done.
  66:      throw new SPException(string.Format("Display group '{0}' not found.", g));
  67:     }
  68:     group.Add(scope);
  69:     group.Update();
  70:    }
  71:   }
  72:  }
  73:  
  74:  return 1;
  75: }

The syntax of the command can be seen below:

C:\>stsadm -help gl-updatesearchscope

stsadm -o gl-updatesearchscope

Updates the specified search scope for a given site collection.

Parameters:
        -url <site collection url>
        -name <scope name>
        [-description <scope description>]
        [-groups <display groups (comma separate multiple groups)>]
        [-searchpage <specific search results page to send users to for results when they search in this scope>]

Here's an example of how to update a web application to assign the shared scope created above to appropriate groups:

stsadm –o gl-updatesearchscope -url "http://intranet" -name "Search Scope 1" -groups "search dropdown, advanced search"

3. gl-addsearchrule

Once you have a search scope created you can now add rules to it. This command is slightly more complex due to the different types of rules that can be created. In general there are four types: AllContent, ContentSource, PropertyQuery, and WebAddress. The ContentSource is typically only used with shared scopes (you can create a ContentSource rule on a scope that is not shared using this tool but you cannot do it via the browser - I'm honestly not sure if the rule will work correctly though). To manage the rules of a scope you simply grab the Rules property of the Scope object and call the appropriate method (there's one for each type of rule except for ContentSource which is effectively just a PropertyQuery rule that uses the ContentSource managed property):

   1: public override int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
   2: {
   3:  output = string.Empty;
   4:  
   5:  InitParameters(keyValues);
   6:  
   7:  string url = Params["url"].Value.TrimEnd('/');
   8:  string scopeName = Params["scope"].Value;
   9:  PageType type = (PageType)Enum.Parse(typeof(PageType), Params["type"].Value, true);
  10:  
  11:  ScopeRuleFilterBehavior behavior = ScopeRuleFilterBehavior.Include;
  12:  if (Params["behavior"].UserTypedIn)
  13:   behavior = (ScopeRuleFilterBehavior)Enum.Parse(typeof(ScopeRuleFilterBehavior), Params["behavior"].Value, true);
  14:  
  15:  
  16:  using (SPSite site = new SPSite(url))
  17:  {
  18:   SearchContext context = SearchContext.GetContext(site);
  19:   Scopes scopeManager = new Scopes(context);
  20:   Scope scope = scopeManager.GetScope(new Uri(site.Url), scopeName);
  21:   Schema schema = new Schema(context);
  22:  
  23:   switch(type)
  24:   {
  25:    case PageType.AllContent:
  26:     scope.Rules.CreateAllContentRule();
  27:     break;
  28:    case PageType.ContentSource:
  29:     scope.Rules.CreatePropertyQueryRule(behavior, schema.AllManagedProperties["ContentSource"], Params["propertyvalue"].Value);
  30:     break;
  31:    case PageType.PropertyQuery:
  32:     ManagedProperty prop;
  33:     try
  34:     {
  35:      prop = schema.AllManagedProperties[Params["property"].Value];
  36:     }
  37:     catch (KeyNotFoundException)
  38:     {
  39:      throw new SPException(
  40:       string.Format("Property '{0}' was not found.", Params["property"].Value));
  41:     }
  42:     scope.Rules.CreatePropertyQueryRule(behavior, prop, Params["propertyvalue"].Value);
  43:  
  44:     break;
  45:    case PageType.WebAddress:
  46:     UrlScopeRuleType webType =
  47:      (UrlScopeRuleType) Enum.Parse(typeof (UrlScopeRuleType), Params["webtype"].Value, true);
  48:     scope.Rules.CreateUrlRule(behavior, webType, Params["webvalue"].Value);
  49:     break;
  50:   }
  51:  }
  52:  
  53:  return 1;
  54: }

The syntax of the command can be seen below:

C:\>stsadm -help gl-addsearchrule

stsadm -o gl-addsearchrule

Adds a search scope rule to the specified scope for a given site collection.

Parameters:
        -url <site collection url>
        -scope <scope name>
        -behavior <include | require | exclude>
        -type <webaddress | propertyquery | contentsource | allcontent>
        [-webtype <folder | hostname | domain>]
        [-webvalue <value associated with the specified web type>]
        [-property <managed property name>]
        [-propertyvalue <value associated with the specified property or content source>]

Here's an example of how to add a rule to the scope created above which will prevent content from the HR site collection from being returned in the results:

stsadm –o gl-addsearchrule -url "http://intranet" -scope "Search Scope 1" -behavior exclude -type webaddress -webtype folder -webvalue "http://intranet/hr"