Compass: Role based searching using CompassQueryFilter

December 18th, 2009 by  |  Published in Compass, Java  |  2 Comments

While implementing a forum with wicket, spring, hibernate and compass for search, I recently ran into a problem: there are topics and posts that should only visible for some users. Say there’s a moderator forum where all content should only be visible for … well moderators :-).

We’re using role based authentication in our apps,  where each User object has one role that can in turn imply other roles. I’m leaving out some method, but you can imagine the IRole interface like that:

public interface IRole {
public List getImpliedRoles();
 
public boolean hasRole(final IRole role);
}

we’re using an implementation that’s quite close to this:

public enum SessionRole implements IRole {
 
ANONYMOUS(),
USER(),
MODERATOR(USER),
ADMIN(MODERATOR);
 
private final List _impliedRoles;
 
private SessionRole() {
   _impliedRoles = Collections.emptyList();
}
 
private SessionRole(final IRole... impliedRoles) {
    _name = createName();
 
   final HashSet set = new HashSet();
   for (final IRole impliedRole : impliedRoles) {
   set.add(impliedRole);
   set.addAll(impliedRole.getImpliedRoles());
}
 
_impliedRoles = Collections.unmodifiableList(new ArrayList(set));
}
 
public List getImpliedRoles() {
return _impliedRoles;
}
 
public boolean hasRole(final IRole role) {
return this == role || _impliedRoles.contains(role);
}
 
}

And for the sake of completeness, here the indexed entity (only showing the relevant fields):

public class ForumEntry {
 
private String _headline;
 
private String _text;
 
private IRole _readRole;
 
}

The visibility of the forum wasn’t a problem at all – where it got complicated was when performing a search. Of course the lucene index contains all posts and thus the search runs over all entries and also returns all entries – leading to results showing up for regular users that should be for moderators’ eyes only.

The first ideas that came to my mind (and also the first I abandoned) were:

  1. Performing a regular search without considering any roles at all, then sort out the ones the user isn’t allowed to see
  2. Save all roles that are allowed to see the entry to the ForumEntry
  3. Add a hook into the indexing process and save the moderator entries in an other index (probably the worst idea)

1. is quite bad as far as design is concerned and not really handy when it comes to paging through the results, 2. would bloat the index a bit and each IRole would not only have to know its implied roles, but also its inferring roles (the roles that are higher in hierarchy). E.g. USER would have to know that MODERATOR and ADMIN have at least the same rights. This might seem easy for this case but gets complicated with a more complex role structure. 3. no that’s really bad … we want to use the same index (and also the same UI) for all searches.

The solution is pretty simple to implement but was very hard to find: CompassQueryFilters. As the name suggests, a CompassQueryFilter is

A filter used to filter out query results.

For filtering my ForumEntry entities based on the user role, I use a boolean query builder and add a should-rule for each implied role and for the role itself. A should role for a boolean query applies when at least one of the rules applies to the matching hits. The code goes something like that:

CompassSession session = ...;
IRole userRole = ...;
 
final CompassQueryBuilder build = session.queryBuilder();
final CompassQueryStringBuilder queryStringBuilder = build.queryString(qry);
final CompassQuery q = queryStringBuilder.toQuery();
final CompassQueryFilterBuilder queryFilterBuilder = session.queryFilterBuilder();
final CompassQueryBuilder.CompassBooleanQueryBuilder booleanQueryBuilder = build.bool();
 
if (userRole.getImpliedRoles() != null) {
for (final IRole r : userRole.getImpliedRoles()) {
booleanQueryBuilder.addShould(build.term("readRole", r.toString().toLowerCase()));
}
}
booleanQueryBuilder.addShould(build.term("readRole", userRole.toString().toLowerCase()));
 
final CompassQueryFilter queryFilter = filterBuilder.query(booleanQueryBuilder.toQuery());
q.setFilter(queryFilter);

One last caveat: the indexed IRole field of ForumEntry cannot be empty or null, the whole filtering process would then yield wrong results. A simple solution is to add a base role to the existing roles which every role implies, something like that:

public enum SessionRole implements IRole {
 
DEFAULT(),
ANONYMOUS(DEFAULT),
USER(DEFAULT),
MODERATOR(USER),
ADMIN(MODERATOR);
 
}

Responses

  1. Mak says:

    March 8th, 2010 at 9:16 am (#)

    what if a user’s role changes? Do you reindex? I am looking for a similar solution but in my application, a users’ role changes often and rebuilding the index does not make sense. I guess your suggestion 1 is the best in my case. Do you have any more suggestions? :)

  2. Michael Sparer says:

    March 8th, 2010 at 10:28 am (#)

    Hey Mak,

    it doesn’t matter how often a user’s role changes as I only index the roles _required to read an entry_.

Leave a Response