A slight alternative to the suggestion from Klaus:
In Sitecore.ContentSeach.config
you'll find a pipeline called contentSearch.getGlobalSearchFilters
Processors added to this pipeline will be applied to any query, so if we drop in one that applies a filter based on roles we're good.
ComputedField
To start, we want a computed field added to our index configuration:
<fields hint="raw:AddComputedIndexField">
<field fieldName="read_roles" returnType="stringCollection">Sitecore.ContentSearch.ComputedFields.ReadItemRoles,Sitecore.ContentSearch</field>
</fields>
NOTE the stored type is a collection of strings. We'll use it to index all the names of roles that can read an item.
Implementation
We have a base abstract class to handle the extraction of item security details
public abstract class ItemPermissions: IComputedIndexField
{
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 GetPermissibles(security);
}
protected abstract object GetPermissibles(ItemSecurity security);
}
We implement the above with the abstracted method
public class ReadItemRoles : ItemPermissions
{
protected override object GetPermissibles(ItemSecurity security)
{
var roles = RolesInRolesManager.GetAllRoles();
return roles.Where(security.CanRead).Select(r => r.Name);
}
}
NOTE There's obviously a performance impact here, this will reduce your indexing speed.
To reduce the impact, only add the the computed field to the index configuration for the index that contains secured content. E.g. If your web content is only accessed by the anonymous user it will add no benefit.
Pipeline
Add the entry in to the config
<contentSearch.getGlobalSearchFilters>
<processor type="Sitecore.ContentSearch.Pipelines.GetGlobalFilters.ApplyGlobalReadRolesFilter, Sitecore.ContentSearch" />
</contentSearch.getGlobalSearchFilters>
Implementation
Implement the pipeline filter to check the roles of the context user
public class ApplyGlobalReadRolesFilter : GetGlobalFiltersProcessor
{
public override void Process(GetGlobalFiltersArgs args)
{
var query = (IQueryable<SitecoreUISearchResultItem>)args.Query;
var userRoles = Context.User.Roles.Select(r => r.Name.Replace(@"", @""));
var predicate = PredicateBuilder.True<SitecoreUISearchResultItem>();
predicate = userRoles.Aggregate(predicate, (current, role) => current.Or(i => i["read_roles"].Contains(role)));
if(predicate.Body.NodeType != ExpressionType.Constant)
args.Query = query.Filter(predicate);
}
}
Summary
- Create a ComputedField that returns a list of all valid roles for a given access right
- Apply a pipeline processor to
contentSearch.getGlobalSearchFilters
to add a query filter to each search request.
- Use the
PredicateBuilder
class to ensure the role names are OR'ed together
The big benefit here is that you take the hit at index time and the handling of item restriction is handled through a search query as normal. No need to worry about the facet numbers or search counts being incorrect.
You can restrict the roles you are checking to compute the field and you can vary the application of the pipeline filter. You can even take out the pipeline filter and just update your queries to filter when you require it.
NOTE The biggest problem with this set up is the requirement to re-index your content when security restrictions change. Should you be applying security restrictions to users themselves, you'll have to include additional computed fields.
Edit 02/06/2013
I was just tinkering with this in a project and realised that it was AND'ing the roles in the query. If a user had multiple roles assigned then both roles would have to have declared rights to the item. I've updated the pipeline processor to use the PredicateBuilder
class to OR the roles. A check is also added to ensure the predicate is not a constant, this ensures the query is updated only if we have a filter to apply.