Search filter component extension
Bravura Security Fabric ships with a Python IDMLib library plugin extension for search filters. The plugin is intended to be used via the component framework.
Basic Usage
Add an extension to your component manifest as follows:
<extension type="plugin"> <filename>extension_script.py</filename> <provides>SEARCH_FILTER_PLUGIN</provides> <provides_for> <searchtype>SEARCHTYPE</searchtype> </provides_for> </extension>
SEARCHTYPE
should be the type of search engine you want your filter to run forextension_script.py
is the name of your search filter extension script
The idmlib.plugins.searchfilter.criteria
component contains classes pre-configured with the available search criteria for many common search engines. Look for the class with a _search_type
matching the engine you defined in the manifest.
List criteria are a more limited interface that allows you to explicitly show (include) or hide (exclude) uniquely identified objects from a particular search. Although more limited, this method is much more performance efficient if the logic required to filter the objects using standard search criteria would be very complex.
Only certain search types support list criteria (generally only those where it is possible to uniquely identify every available object the search applies to), a given search criteria class supports list criteria if it has _supports_lists
as True. When passing criteria to list criteria methods, you must provide a value for all the keys in _key_fields
; values for any other fields will be ignored.
To get an object you can pass to the list criteria methods you should use the instance method of your criteria class. You can pass the values for the required fields either as keyword arguments to instance, or by setting them as attributes afterwards.
The two most important list criteria methods are include and exclude. Objects passed to include will be shown, and objects passed to exclude will be hidden. Here’s a short example:
from idmlib.components.extension import ExtPlugin from idmlib.plugins.searchfilter import SearchFilter from idmlib.plugins.searchfilter.criteria import CriteriaRole class ListSearchFilter(ExtPlugin): def __init__(self, plugin: SearchFilter, state: dict): super().__init__() self.plugin = plugin self.state = state def process(self): for role in {"IT", "ADMIN", "MAINTENANCE"}: self.plugin.include(CriteriaRole.instance(roleid=role)) for role in {"HR", "CUSTOMER\_SERVICE"}: self.plugin.exclude(CriteriaRole.instance(roleid=role))
Search criteria allow you to filter what is visible to the user using search queries. Individual criteria are simple: A field to compare, a comparison to use, and a value to compare against. These individual criteria can be combined to build more complex queries, but the combination is limited, groups of criteria will combine with AND, and different groups will then be combined with OR. This limitation requires that you break down complex logic and think carefully about the result you want the sum of your criteria to have. Additionally, while powerful, search queries are not incredibly performant, if you find yourself emitting several hundred criteria for a particular search engine, you may want to look at refactoring to use list criteria (if supported) as filtering on that many criteria will start to slow down the UI, and hurt the experience of the end user.
Search criteria are created by operating on the provided criteria class, or by calling methods named after the particular comparison you want to use; for example: equals/==, less_than/before/<, is_/>>
and so on.
from idmlib.components.extension import ExtPlugin from idmlib.plugins.searchfilter import SearchFilter from idmlib.plugins.searchfilter.criteria import CriteriaUser class MySearchPlugin(ExtPlugin): def __init__(self, plugin: SearchFilter, state: dict): super().__init__() self.plugin = plugin self.state = state def process(self): for target in {"AD", "AD_TWO", "EXTERNAL"}: for group in {"GROUP_ONE", "GROUP_TWO"}: self.plugin.builder.get( f"{target}-{group}-filter" ).add( CriteriaUser.hostid == target, CriteriaUser.groupid == group, CriteriaUser.managerid == "ImportantManager", )
The resulting filter will display only Users where:
HostID: AD, and GroupID: GROUP_ONE, and ManagerID: ImportantManager
(or) HostID: AD, and GroupID: GROUP_TWO, and ManagerID: ImportantManager
(or) HostID: AD_TWO, and GroupID: GROUP_ONE, and ManagerID: ImportantManager
(or) HostID: AD_TWO, and GroupID: GROUP_TWO, and ManagerID: ImportantManager
(or) HostID: EXTERNAL, and GroupID: GROUP_ONE, and ManagerID: ImportantManager
(or) HostID: EXTERNAL, and GroupID: GROUP_TWO, and ManagerID: ImportantManager
Or 6 filters (AD-GROUP_ONE-filter, AD-GROUP_TWO-filter, AD_TWO-GROUP_ONE-filter, ...) each containing 3 criteria, for a total of 18 criteria.
Complete Interface
The FilterBuilder is the core of how to build search filters. It provides a single, restrictive, validated method for constructing search filters.
A FilterBuilder with the correct search criteria validation information is created for you at plugin.builder during the creation of the SearchFilter plugin object.
The main logic of the FilterBuilder works as follows:
Criteria under the same filter (with the same key/added to the same FilterEditor) are combined using a logical AND
Different filters (groups of criteria using different keys) are combined using logical OR
Because criteria are simple, criteria within a given filter can be reordered, and duplicate criteria removed, and because separate filters are combined with OR their order also does not matter. Example:
(a and b) or (c and d and e) == (e and d and c) or (b and a)
Users are expected to create/get a filter with a unique key, and then add/remove/replace criteria within it using the
FilterEditor
interface
FilterBuilder has the following public methods:
valid_search_key(key: str, field: Optional[str] = None) -> bool
Checks that a given key or key/field combination is available for the given run of the plugin, True if available, False otherwise.
Useful if you want to ensure that a given field (Account/Resource Attribute, etc.) is available in the product prior to filtering on it, as attempting to filter on a non-existent criteria/field will cause the builder to throw an exception when added.
Example:
if plugin.builder.valid_search_key('ma_attrkey', 'RESATTR_TEAM'): …
create(key: str) -> FilterEditor
Creates a filter for the given key, and returns a FilterEditor object with which to manipulate the filter. (FilterEditor interface described later)
Throws KeyError if you try to create a filter that already exists.
Example:
plugin.builder.create('MY_FILTER').add(CriteriaClass.criteria == True)
get(key: str, create: bool = True) -> FilterEditor
Retrieves a FilterEditor for an existing filter.
If create=True (the default) creates a filter if it doesn’t already exist, returning a FilterEditor for it.
If create=False throws KeyError if a filter with that key does not already exist.
Useful for updating/editing an existing filter, or for creating a filter if this may not be the first component to do so.
Can be used to supply incremental criteria to common filters.
Example:
plugin.builder.get('_NON_COMPONENT_ACCOUNTS_').add(CriteriaPAMResources.
ma_attrkey.RESATTR_PERSONAL_OWNER >> 'EMPTY')
remove(self, key: str) -> bool
Removes a filter and all its criteria from the builder. Returns True if something was removed, False otherwise.
search_regex(pattern: str) -> Iterable[Tuple[str, FilterEditor]]
Retrieves the (key, filter_editor) of any existing filters with keys matching the regular expression pattern string.
Useful if you want to edit or remove multiple filters following a specific naming convention.
Example:
for name, filter_editor in plugin.builder.search_regex('^REQUESTER_TEAM='): filter_editor.add(...)
search_expressions(⋆expressions: SearchExpression) -> Iterable[Tuple[str, FilterEditor]]
Retrieves the (key, filter_editor) of any existing filters that contain all the criteria passed in.
Useful if you want to edit or remove any filters acting on certain conditions.
Example:
for name, filter_editor in list(plugin.builder.search_expressions
((CriteriaClass.flag_one == True), (CriteriaClass.flag_two == False))): plugin.builder.remove(name)
The conversion to list (or some other container type) is required here, to avoid having the internals of the builder complain and throw
RuntimeError: dictionary changed size during iteration
due to the call to remove.
A FilterEditor is a collection of SearchExpressions, with public methods for adding/removing/replacing individual expressions, and validation logic to ensure an expression is valid for a given search. A SearchExpression is an individual criteria comparison, the result of using an operator on a SearchKey , more details later, but the below examples should be good enough to get an idea of how to work with them.
Any public method on a FilterEditor returns the same editor, so you can ’chain’ operations together or store the editor for later use, add and remove also support any number of arguments, allowing you to pass multiple SearchExpressions to them.
It has the following public methods:
add(self, *expressions: SearchExpression) -> Self
Add the given expressions to the filter, the provided expressions will be validated, and duplicate expressions will be ignored.
Throws ValueError on invalid expressions.
Example:
filter.add(CriteriaClass.criteria_one == 'value') .add(CriteriaClass.criteria_two == False)
(equivalent) Example:
filter.add(CriteriaClass.criteria_one == 'value', CriteriaClass.criteria_two == False)
remove(self, *expressions: SearchExpression) -> Self
Remove the given expressions from the filter, the provided expressions are validated, missing expressions are ignored.
Throws ValueError on invalid expressions.
Example:
filter.remove(CriteriaClass.criteria_one == 'value')
replace(self, old: SearchExpression, new: SearchExpression) -> Self
Replaces the old expression with a new expression, both expressions are validated.
Throws a KeyError if the old expression is not in the filter.
Throws ValueError on invalid expressions.
Example:
filter.replace((CriteriaClass.criteria_two == False),
(CriteriaClass.criteria_two == True))
exclude(self, *criteria: List[SearchCriteria.CriteriaDict], tag: str = DEFAULT_TAG)
include(self, *criteria: List[SearchCriteria.CriteriaDict], tag: str = DEFAULT_TAG)
List criteria passed to these methods are grouped with a ’tag’ (very similar to the builder’s filter groups). Criteria in different tags is unioned together during plugin close. The methods can accept multiple list criteria at once, making it easy to add a large number of criteria at one time.
exclude_and(self, *criteria: List[SearchCriteria.CriteriaDict], tag: str = DEFAULT_TAG, add_if_empty: bool = False)
include_and(self, *criteria: List[SearchCriteria.CriteriaDict], tag: str = DEFAULT_TAG, add_if_empty: bool = False)
Two new methods were added, one for inclusion, and another for exclusion, but both function the same. These methods are designed for the use case where one component wants to reduce/further restrict the list criteria added by another, but does not want to have to re-do the same calculations as the other component in order to do so.
The _and
methods are basically a set intersection (the criteria in the tag is reduced to the criteria in the input and in the tag).
Example:
plugin.include(criteria_a, criteria_b)
plugin.include_and(criteria_b, criteria_c)
The resulting plugin output will contain only criteria_b as include criteria.
If add_if_empty is True, and you call an _and method on an empty tag, then instead of resulting in the tag remaining empty (since it’s a set intersection), the criteria is instead added to the tag as though you had just called include/exclude themselves. In the above example, provided we always knew that both involved components would always add some number of list criteria, both could instead call include_and with add_if_empty set to True, allowing the component priorities to be re-ordered without any logic needing to be changed.
Example: (equivalent to previous example)
plugin.include_and(criteria_a, criteria_b, add_if_empty=True)
plugin.include_and(criteria_b, criteria_c, add_if_empty=True)
The resulting plugin output will contain only criteria_b as include criteria
Without
add_if_empty=True
the plugin would output no include criteria, since the DEFAULT_TAG would initially contain nothing.
This is why tagging is useful, as without it it would quickly be very difficult for multiple components (operating on the same search) to take advantage of this feature if they could not adequately separate what criteria they cared about.
A SearchExpression is a Python dataclass representation of a single filter group in the KVG, minus the position and operator. It is read only, and cannot be altered once created. It has a single public method: negate() which returns a new expression with the negated value flipped, it is equivalent to doing ~SearchExpression, since it also implements the invert operator. The value of SearchExpression().comparator must be a value from the SearchExpression.Comparator enum, however you should never create expressions directly outside of testing.
A SearchKey can be thought of as a generator class for SearchExpressions. It will generate the expected SearchExpression when used with a comparison operator and a value, the SearchKey should always be the left side of the comparison. SearchKeys can be initialized a few ways:
SearchKey('criteria_key')
- Generates SearchExpressions for 'criteria_key' onlySearchKey('criteria_key', field='criteria_field')
- Generates SearchExpressions for 'criteria_key' with the field 'criteria_field' onlySearchKey(’criteria_key’, complex=True)
- Generates SearchExpressions for 'criteria_key' but will also generate SearchExpressions for fields if operated on with SearchKey('criteria_key, complex=True).criteria_field etc. This is done by dynamically returning SearchKey('criteria_key', field='criteria_field') when you access the attribute.
To access nested attributes of complex SearchKeys that contain @ or - use _AT_ and _DASH_ instead. Example: S earchKey(’accountattr’, complex=True)._AT_accountEnabled
SearchKeys support the following operators/methods:
Equals / ==
On / ==
Only valid for datetime.date objects
Less than / <
Before / <
Only valid for datetime.date objects
Less than or equals / <=
Before or on / <=
Only valid for datetime.date objects
Greater than / >
After / >
Only valid for datetime.date objects
Greater than or equals / >=
After or on / >=
Only valid for datetime.date objects
Is / >>
Due to reserved keywords, the method version is called is_
Contains /
Accepts * and ? wildcards
Starts with / ^
Accepts * and ? wildcards
SearchCriteria is the base class for capturing the available SearchKeys for a given engine. Commonly used Criteria classes can be found in idmlib.plugins.searchfilter.criteria. A SearchCriteria class is a collection of SearchKeys, representing a single search type. However, it has a few interesting properties in order to support the use of list criteria:
If you want to use a SearchCriteria class with include/exclude you need to get a modifiable CriteriaDict instance specialized for that particular SearchCriteria subclass, this can be done by calling SearchCritera.instance()
You can set values on attributes of this returned object to specify the values to include/exclude, then pass them to the applicable function.
You can also provide values directly on instantiation by passing them as keyword arguments to
instance
.
In order to support include/exclude a SearchCriteria subclass must:
Set the
_search_type
class attribute to a valid search typeSet
_supports_lists
to TrueProvide a dictionary to
_key_fields
mapping the Python name of the class attribute to the KVG name in the list output.
Additionally, the following property helps in cases where some keys may be defined in the plugin input, but the class has not yet been updated with the new keys:
If the class does not explicitly specify a SearchKey class attribute for a given attribute, one will be created for you on access, it will automatically set complex to True, and have the same name as the accessed attribute, but _AT_ will be replaced by @, and _DASH_ with -, similar to nested members of complex SearchKeys. Example:
Accessing S earchCriteria.some_DASH_criteria
would generateSearchKey('some-criteria', complex=True)
.