August 28, 2024

IIQCommon overview: match anything with the Hybrid Object Matcher

This will be part of a blog series providing an overview of the many SailPoint IIQ utilities and tools available in Instrumental ID’s open-source iiq-common-public library. (For an overview of the entire library, see this earlier post.)

This post will be about the “Hybrid Object Matcher” utility. This utility expands on the matching tools available within SailPoint IIQ, providing a robust way to match anything with any SailPoint IIQ filter.

You will need to download or build that library’s JAR file and include it in your IIQ installation, either at web/WEB-INF/lib in an SSB repository or at WEB-INF/lib directly in Tomcat.

Quick Example

First, a quick example, so you can see what this utility will accomplish. The following method will return true if the Identity has a Sponsor whose manager’s title is Director, as well as an account in either Active Directory or Workday HR. This seems straightforward enough, but the implementation behind it turns out to be extremely complex. HybridObjectMatcher makes it easy.

public boolean matchIdentity(SailPointContext context, Identity toCheck) {

    Filter filter = Filter.and(
        Filter.eq("sponsor.manager.title", "Director"),
        Filter.in("links.application.name", List.of("Active Directory", "Workday HR"))
    );

    HybridObjectMatcher matcher = new HybridObjectMatcher(context, filter);

    // In theory, any Java object could be matched here, not
    // only those provided by IIQ.
    return matcher.matches(toCheck);
}

The concepts

This gets a bit esoteric, so bear with me. Computer Science incoming! (Feel free to skip down to the “IIQCommon Matcher” section, if you aren’t interested in the gory details.)

Matcher

The goal of a Matcher is to say whether an arbitrary object matches some criteria. IIQ ships with a very generic interface sailpoint.search.Matcher, which exposes the following method: boolean matches(Object something) throws GeneralException.

IIQ also provides a variety of Matcher implementations. Usually, the matching criteria is expressed as a Filter object, such that an object will match if the search query expressed by the Filter would find the object. The matching between Object and Filter is generally done in-memory, but you could also write a Matcher that translates part of the Filter into a SQL query and runs that.

Filters

Remember trees from Computer Science?

IIQ Filter objects are nested as a tree, and are thus either a leaf node or a composite node. Leaf nodes are operations like “X equals Y” or “XY starts with X”. Composite nodes implement AND / OR / NOT operations.

Filters can be nested arbitrarily deeply.

A Filter tree structure, indicating that the name can be either “John” or “Jack” AND the department is in the given set

FilterVisitor

To ease the translation between a Filter tree and something else, IIQ supplies an interface implementing the Visitor design pattern: Filter.FilterVisitor. Visitors separate navigation of a tree structure from the business logic you want to apply to the tree. Visitors are stateful, meaning that they will probably be accumulating something from all those tree nodes that they’ll return at the end. The FilterVisitor interface has one method for each type of Filter: visitEq, visitNe, visitAnd, and so forth.

The actual invocation of the Visitor follows the usual design pattern, which involves some back-and-forth. Each instance of Filter exposes an accept method, which is passed your instance of FilterVisitor. The Filter then invokes the correct method on FilterVisitor, passing itself.

Composite filters might invoke FilterVisitor.visitAnd(this), which will then apply the Filter to each of the nested Filters. A leaf filter might invoke FilterVisitor.visitEq(this).

// Simple example of what this might look like on a FilterVisito
public void visitAnd(CompositeFilter andFilter) {
  // Traverse the tree
  for(Filter child : andFilter.getChildren()) {
    // This will modify the state of the FilterVisitor somehow
    child.accept(this);
  }

  // Do something here with the outcome of all of those child items
}

There are two concrete implementations of FilterVisitor that you have likely used if you’ve done any IIQ development.

  • IIQ uses the FilterVisitor to implement the built-in Filter.getExpression() method, which returns the original Filter String representation of the tree. The implementation of FilterVisitorFilterToExpressionVisitor in this case – maintains a StringBuilder that appends the string query as it is passed around through the Filter tree. Once the visitation is completed, the visitor object has the complete string representation and can return it.
  • A similar process translates Filters into nested HQL queries when querying the database via context.search(). For example, a Filter.eq leaf node will be translated into something like i.name = 'Some value' in HQL or SQL.

Matcher + FilterVisitor

Time to combine our two concepts!

In the case of a Matcher, each visit will apply the Filter to the object we intend to match. We will retain some state indicating whether the Filter matches. For example, consider the following filter:

name == "jsmith" && manager.name == "vishal"

In object terms, this would become a tree like this:

                / Equals("name", "jsmith")
Composite(AND) 
                \ Equals("manager.name", "vishal")

Our FilterVisitor would be applied to the composite “AND” first, causing the Visitor’s visitAnd to be invoked. That method ought to begin with a state of “does not match”, and then apply the Visitor (recursively) to each of the Equals expressions. Those would call visitEq, which would do whatever is needed to apply the search to the object, returning “matches” or “does not match”. Finally, back in visitAnd, we would determine that if all of the child Filters match, then the AND matches.

Built-in Filter Matchers

Since this concept is used so heavily within IIQ itself, the product naturally ships with a number of Matcher + FilterVisitor classes.

In general, these are used as follows:

// the Filter itself, along with perhaps other stuff, is passsed to the constructor
Matcher matcher = new SomeMatcher(filter);

// matching is done via the interface method
boolean matches = matcher.matches(object);

Built-in implementations include, in roughly increasing order of complexity:

  • MapMatcher: Determines whether a Filter matches the object structure within an ordinary Java Map. The Map can contain nested strings, lists, or other similar basic Java objects.
  • JavaMatcher: An abstract class intended to read properties from a Java object, applying the filters to values pulled directly from the Object. The actual “read properties from the object” is left to the sub-class, which we will take advantage of later.
  • CubeMatcher: Part of the Matchmaker API, this JavaMatcher is used to match your role membership criteria. It can read values from the configured Links as well as follow some types of nested attribute paths, such as “manager.name”.
  • ReflectiveMatcher: Another JavaMatcher implementation, this one uses Apache BeanUtils to read values from arbitrary Java objects for comparison.
  • HybridReflectiveMatcher: We have arrived at our parent class! One problem with the various in-memory matchers above is that they cannot support subquery searches that are trivial when a Filter is translated into HQL. The HybridReflectiveMatcher implements a subset of this behavior, allowing an in-memory search to go out to the database if needed.

The IIQCommon matcher

In attempting to use the various SailPoint-provided Matchers, I came across several shortcomings with the existing implementations, all of which are resolved by the HybridObjectMatcher in IIQCommon. The good news is that the OOTB Matcher implementations are really good, so all I needed to do was extend HybridReflectiveMatcher and fix the problems!

Null safety: Null values in a path cause NullPointerExceptions with some of the Matcher implementations.

Object lookups: We want to be able to support constructs like Filter.eq("manager.id", managerObject) and Filter.eq("manager", managerId), both of which are supported by the HQL parser.

List path navigation: We want the value of manager.links.application.name to be a List, containing the application name of all Link objects belonging to the manager.

HQL list semantics: Using context.search, you can write a filter like this – Filter.eq("links.application.name", "Active Directory") – and it will work. IIQ will translate it into an HQL join, so the query will match if any row matches. The same is true of Filter.in and Filter.like queries. IIQ’s Matcher implementations don’t support this, because links.application.name is a either a List or a List’s toString, which will never match the string “Active Directory”.

Silly assertion code: The HybridReflectiveMatcher has a Java assertion forcing the SailPointContext to be an instance of a particular class. This does not allow Instrumental ID’s testing suite to work properly.

Translation to an HQL-like “contains”

When the property being matched is a List, and any eq, in, or like operation fails to match, the matcher will try again using a “contains”. Each item in the list will be compared with the Filter value, and the Filter will match if any item matches.

Attribute paths

Attribute paths do not use Apache BeanUtils directly. Instead, they use IIQCommon’s Utilities.getProperty, which offers some advanced path structures and shortcuts for common paths.

You could match on the following sorts of property strings:

  • name (String)
  • manager.name (String)
  • assignedRoles.name (List)
  • links.cn or links.attributes[cn] or links..attributes.cn (all the same, List)
  • links[0].cn (String)

See that method for details! I will also write another blog post with details about the property path logic.

Object property lookup

If you set allowObjectPropertyEquals to true on your HybridObjectMatcher, a special mode will allow you to match properties on one object against properties of another, all in-memory. This allows very SQL-like behavior for little cost.

For example, you could do: Filter.eq("department", "manager.department"). The Matcher would first try the literal string “manager.department”, which would invariably fail to match. It would then resolve that path, finding the department of the manager, and try to match that value.

This works for eq, ne, in, and containsAll queries.

When would I use this?

Great question. Really, you would use this any time SailPoint’s database querying capabilities aren’t specific enough. There aren’t always enough searchable fields, and you can’t always express your query in an HQL-friendly way.

Turns out that we use it all the time!

For example:

  • We most broadly use HybridObjectMatcher within Beanshell role membership criteria, because you can express more detail than you can with IIQ’s default filters. Specifically, the filters are written into a single configuration file and a static Beanshell rule interprets the configuration for the current role. Users of our Complex Role Membership Plugin (CRMP) will be familiar with this setup.
  • We use them for authorization in our plugin web services, along the lines of QuickLink Populations.
  • We use them in many tools for including or excluding certain objects from an already-loaded list. For example:
    • IdentityLinkUtil can be configured to ignore certain objects from an Identity’s list of Links.
    • Our Loopback Connector, part of the closed-source subset of IIQCommon, uses HybridObjectMatcher filters to include or exclude certain accounts from management by the utility.
    • Our IIQ testing suite uses HybridObjectMatcher in a full-fledged persistence layer, allowing tests to be run outside of a running IIQ environment. This is how IIQCommon’s hundreds of self-tests are written.
    • Our Deploy Plugin, used within our CI/CD pipeline, can use a HybridObjectMatcher filter to include only certain objects of a given time for export.
    • Our UI Enhancer plugin uses the HybridObjectMatcher to enhance searching in its custom User Viewer interface.