What You Will Learn

This blog post will show you how to write your own custom security pre-trimmer for SharePoint 2013. We will take you through the steps of deploying and registering the trimmer before putting the trimmer to work.

Please visit the official MSDN documentation for the overview and
definitive source of documentation of this feature:

http://msdn.microsoft.com/en-us/library/ee819930.aspx

Why Use Pre-Security Trimmers

Pre-trimming refers to pre-query evaluation where the backend rewrites the query adding security information before the index lookup in the search index. Post-trimming refers to post-query evaluation where search results are pruned before they are returned to the user.

We recommend the use of pre-trimming for performance and general correctness; pre-trimming prevents information leakage for refiner data and hit count instances.

Requirements

  • SharePoint 2013 Server
  • Visual Studio 2012
  • A Custom Connector sending claims (see previous post on this blog)

The Trimmer Design

Let's create a simple pre-security trimmer. A trimmer that reads group membership data from a text file, performs a user lookup for group membership data and then adds claims to the query tree based upon this. In short, the trimmer code needs to figure out which user that is issuing the query, then perform a group membership lookup on that user and then add claims for that user, depending on the group membership.

The Code

This MSDN article offers useful starting tips on creating the security pre-trimmer project in Visual Studio, by adding references to both the Microsoft.Office.Server.Search.dll and the Microsoft.IdentityModel.dll.

Add the following to the using directives at the beginning of the class file, SearchPreTrimmer.cs:

    using System.Security.Principal;
    using Microsoft.IdentityModel.Claims;
    using Microsoft.Office.Server.Search.Administration;
    using Microsoft.Office.Server.Search.Query;

We then define the class as implementing the ISecurityTrimmerPre interface in the class declaration:

public class XmlContentSourcePreTrimmer : ISecurityTrimmerPre

We have to define a few constants at the beginning of the class. These variables may be altered by the static properties given when the trimmer is registered with SharePoint.

    private string _claimType = "http://surface.microsoft.com/security/acl";
    private string _claimIssuer = "customtrimmer";
    private string _claimValueType = ClaimValueTypes.String;
    private string _lookupFilePath = "datafile.txt";

The initialization method of the trimmer may modify a few "constant variables", primarily claim type and issuer along with the file path to the input data of this trimmer's group membership data:

    /// <summary>
    /// Initialize the pre-trimmer.
    /// </summary>
    /// <param name="staticProperties">Static properties configured for the trimmer.</param>
    /// <param name="searchApplication">Search Service Application object</param>
    public void Initialize(NameValueCollection staticProperties, SearchServiceApplication searchApplication)
    {
        if (staticProperties.Get("claimtype") != null)
        {
            _claimType = staticProperties.Get("claimtype");
        }

        if (staticProperties.Get("claimissuer") != null)
        {
            _claimIssuer = staticProperties.Get("claimissuer");
        }

        if (staticProperties.Get("datafile") != null)
        {
            _lookupFilePath = staticProperties.Get("datafile");
        }

        RefreshDataFile();
    }    

The AddAccess method of the trimmer is responsible for returning claims to be added to the query tree. We will refresh the group membership data if needed and figure out the user id for key lookup into the group membership structure from the text file. 

    /// <summary> 
    /// Add custom claims to the query tree 
    /// </summary>
    /// <param name="sessionProperties">Session properties collection</param>
    /// <param name="userIdentity">query user identity</param>
    /// <returns>An enumerable of tuples with claims</returns>
    public IEnumerable<Tuple<Claim, bool>> AddAccess(IDictionary<string, object> sessionProperties, IIdentity userIdentity)
    {

        if (null == userIdentity)
        {
            throw new NullReferenceException("Error: AxdAccess method is called with an invalid user identity parameter");
        }

        RefreshDataFile();
        var claims = new LinkedList<Tuple<Claim, bool>>();
        var membership = GetMembership(GetUserId(userIdentity));
        if (membership != null)
        {
            foreach (var member in membership)
            {
                claims.AddLast(new Tuple<Claim, bool>(new Claim(_claimType, member, _claimValueType, _claimIssuer, _claimIssuer), false));
            }
        }

        return claims;
    }

We need a class to act as a simple wrapper of a Dictionary<string, string>. It should essentially serve as a key-value lookup from a user-ID to its group membership data. Each user's group membership has the form group1;group2;group3, where ";" is used to separate between each group membership entry. This class is simply called Lookup.

    private string GetUserId(IIdentity userIdentity)
    {
        // Run through all the claims of the claims identity, look for the
        // user logon name claim and primary SID to get the current user:
        //   domain\username
        ...
        ...

        return strUser;
    }

    private string[] GetMembership(string key)
    {
        lock (_lock)
        {
            var value = _lookup.Get(key);
            if (!string.IsNullOrEmpty(value))
            {
                return value.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
            }

            return null;
        }
    }

    private void RefreshDataFile()
    {
        if ((DateTime.Now - _lookupFileStamp).Seconds > 30)
        {
            lock (_lock)
            {
                _lookupFileStamp = DateTime.Now;
                if (File.Exists(_lookupFilePath))
                {
                    _lookup = Lookup.Load(_lookupFilePath);
                }
            }
        }
    }

Performance Considerations 

Consider the following tips to improve the overall performance with this trimmer as a starting point:

  • Use multiple Lookup classes in a hash-table on a given key.
  • Use a compressible stream and binary serialize the Lookup data.
  • Use IPC (NetPipe) to talk to a local service that holds the more efficient key-value Lookups.

Deploying Trimmer

After you have built the custom security trimmer project, you must deploy it to the global assembly cache on any server in the Query role.

  1. On the Start menu, choose All Programs, choose Microsoft Visual Studio 2010, and then choose Visual Studio Tools and open a Visual Studio command prompt.
  2. To install the SearchPreTrimmer.dll, type the following the command prompt

    gacutil /i <ExtractedFolderPath>\PreTrimmer\bin\Debug\SearchPreTrimmer.dll
  3. As the last step of deploying the trimmer, we need to learn about the token of the DLL. Type the following the command prompt

    gacutil /l SearchPreTrimmer
    Write down the token listed for the newly added DLL.

Registering Trimmer

  1. Open the SharePoint Management Shell.
  2. At the command prompt, type the following command:

    New-SPEnterpriseSearchSecurityTrimmer -SearchApplication "Search Service Application"
    -typeName "CstSearchPreTrimmer.SearchPreTrimmer, CstSearchPreTrimmer,
    Version=1.0.0.0, Culture=neutral, PublicKeyToken=token"

    where token is the text copied from the gacutil /l command above. Example: New-SPEnterpriseSearchSecurityTrimmer -SearchApplication "Search Service Application" -typeName "CstSearchPreTrimmer.SearchPreTrimmer, SearchPreTrimmer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4ba2b4aceeb50e6d" -Id 2
  3. Restart the Search Service by typing

    net restart sphostcontrollerservice

Testing the Trimmer

Now, you can issue queries and you can see the beauty of the pre-trimmer logic in action for every query evaluated. Try modifying the datafile.txt for different group membership per user (keep in mind that contents of the data file are only loaded every 30 seconds). The 30 second refresh interval is a defined constant in the code. Enjoy!

Acknowledgements

Author: Sveinar Rasmussen (sveinar).