How to secure your Lucene Sitecore searches (2.5 ways)

As you may have read in a previous post, I'm creating an extranet where Sitecore extranet users will be able to have an overview of all documents that they have access to.

Sitecore admin users will need to manage who has access to which documents. Documents are stored in the Media Library and a custom index will be used to retrieve the documents for a specific extranet user.

The easiest & fastest way to configure index security

When you occasionally need to retrieve secured items from an index, you can just add the parameter SearchSecurityOptions.EnableSecurityCheck when creating a search context:

var index = ContentSearchManager.GetIndex("yourIndexName");
using (var searchContext = index.CreateSearchContext(SearchSecurityOptions.EnableSecurityCheck))
{
    var theResults = searchContext.GetQueryable<SearchResultItem>()
        .Where(x => x.Language == Sitecore.Context.Language.Name)
        .GetResults(GetResultsOptions.Default);
}

This is a good way if you have an index with mixed content. Having to do searches without a user's security rights in mind and few times with it.

You can also secure the whole index!

If you know that the index only needs to be accessed with security roles in mind, you can change the property defaultSearchSecurityOption to EnableSecurityCheck.

<defaultSearchSecurityOption>EnableSecurityCheck</defaultSearchSecurityOption>

Out of the box, Sitecore comes with 1 index configuration called defaultLuceneIndexConfiguration which is in the file Sitecore.ContentSearch.Lucene.DefaultIndexConfiguration.config.
It has a setting defaultSearchSecurityOption which references a node in the Sitecore.ContentSearch.DefaultConfigurations.config.

<defaultSearchSecurityOption ref="contentSearch/indexConfigurations/defaultSearchSecurityOption" />

References setting:

<!-- DEFAULT SEARCH SECURITY OPTION
This setting is the default search security option that will be used if the search security option is not specified during the creation of search context. The accepted values are DisableSecurityCheck and EnableSecurityCheck. -->
<defaultSearchSecurityOption>DisableSecurityCheck</defaultSearchSecurityOption>

Changing it in that file will set the security option for all indexes that uses the default configuration settings: sitecore_core_index, sitecore_master_index, sitecore_web_index. Which is probably not the thing you want to do.

If you are using a separate, custom created index, you can set the setting in your custom index configuration config file.

<defaultSearchSecurityOption>EnableSecurityCheck</defaultSearchSecurityOption>

But what does this EnableSecurityCheck actually do?

It uses an outbound security filter. Meaning that all results returned from a search query will pass this filter and will be returned or not.
This can create some overhead since every result will be checked if it has the correct security settings applied. And if you have a lot of items in your index, it will slow things down a little bit.

Here's how the outbound security filter from Sitecore works:

public override void Process(OutboundIndexFilterArgs args)
    {
      if (args.IndexableUniqueId == null || !args.IndexableDataSource.Equals("sitecore", StringComparison.InvariantCultureIgnoreCase))
        return;
      ItemUri uri = new ItemUri(args.IndexableUniqueId);
      if (!object.Equals((object) args.AccessRight, (object) this.accessRight.ItemRead()) || Database.GetItem(uri) != null)
        return;
      args.IsExcluded = true;
    }

You can write your own outbound and inbound filters for various purposes.
This great article from Alex Shyba will get you going!

Another way to apply security to your index

You can also create a computed index field with the roles that have read access to the item. You can then directly filter on these roles when building the Linq search query.
Let's have a look on how this can be achieved.

Step 1: Create the ComputedIndexField

public class ItemPermission: IComputedIndexField
{
    public readonly IEnumerable<Role> Roles = RolesInRolesManager.GetAllRoles();
    public string FieldName { get; set; }
    public string ReturnType { get; set; }
    public object ComputeFieldValue(IIndexable indexable)
    {
        var indexableItem = indexable as SitecoreIndexableItem;
        if (indexableItem == null) return null;

        var security = indexableItem.Item.Security;
        return Roles.Where(security.CanRead).Select(r => r.Name);
    }
}

The code above uses RolesInRolesManager.GetAllRoles() to fetch all the non-sitecore roles.
Note that the class must inherit from IComputedIndexField.
The ComputeFieldValue will return all role names that can read the item.
This will then be stored in our index.

Step 2: Adding the computed index field to the index configuration

In the index configuration there is a specific section for Computed Index Fields:

<!-- COMPUTED INDEX FIELDS 
     This setting allows you to add fields to the index that contain values that are computed for the item that is being indexed.
     You can specify the storageType and indextype for each computed index field in the <fieldMap><fieldNames> section.
-->
<fields hint="raw:AddComputedIndexField">
   <!-- existing fields -->
   <field fieldName="__smallcreateddate"             >Sitecore.ContentSearch.ComputedFields.CreatedDate,Sitecore.ContentSearch</field>
   <field fieldName="__smallupdateddate"             >Sitecore.ContentSearch.ComputedFields.UpdatedDate,Sitecore.ContentSearch</field>
   ...
   <!-- Add your custom computed index field! I've called mine readroles -->
   <field fieldName="readroles">Your.Class.Path.ItemPermission, Assembly</field>
</fields>

Ok! Each item in your index should now contain the roles that can read it. (If not, trigger a reindex from the Developer tab in the Sitecore Content Editor.)

Step 3: Searching the index

public List<SearchResultItem> SearchSecuredItems()
{
    //Searching in the sitecore_master_index. Can be a custom index too off course. 
    var index = ContentSearchManager.GetIndex("sitecore_master_index");

    //Getting all the role names of the current user
    var scRoles = Sitecore.Context.User.Roles.Select(x => x.Name).ToList();
    
    using (var s = index.CreateSearchContext())
    {
        //Creating an Or predicate because I want to search a list 
        var predicate = PredicateBuilder.True<SearchResultItem>();
        foreach (var scRole in scRoles)
        {
            //"readroles" must be the same name as defined in the computed index field configuration
            predicate = predicate.Or(x => x["readroles"].Contains(scRole));
        }

        var results = s.GetQueryable<SearchResultItem>()
            .Where(x => x.Language == Sitecore.Context.Language.Name)
            .Where(predicate)
            .GetResults(GetResultsOptions.Default)
            .Select(x => x.Document).ToList();
        return results;
    }
}

And that should be enough to get this working!
If you have any questions, just ask :-)


Useful resources:

This stackoverflow question & answer helped me the most while searching for a good solution for my situation. Check it out!

Other resources: