001package com.identityworksllc.iiq.common; 002 003import java.lang.reflect.InvocationTargetException; 004import java.lang.reflect.Method; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.HashMap; 008import java.util.List; 009import java.util.Map; 010import java.util.Objects; 011import java.util.stream.Collectors; 012 013import sailpoint.api.DynamicScopeMatchmaker; 014import sailpoint.api.EntitlementCorrelator; 015import sailpoint.api.IdentityService; 016import sailpoint.api.Matchmaker; 017import sailpoint.api.SailPointContext; 018import sailpoint.api.SailPointFactory; 019import sailpoint.object.Bundle; 020import sailpoint.object.CompoundFilter; 021import sailpoint.object.DynamicScope; 022import sailpoint.object.Filter; 023import sailpoint.object.GroupDefinition; 024import sailpoint.object.Identity; 025import sailpoint.object.IdentityFilter; 026import sailpoint.object.IdentitySelector; 027import sailpoint.object.Link; 028import sailpoint.object.Profile; 029import sailpoint.object.QueryOptions; 030import sailpoint.object.SailPointObject; 031import sailpoint.object.Filter.BaseFilterVisitor; 032import sailpoint.persistence.HibernatePersistenceManager; 033import sailpoint.search.MapMatcher; 034import sailpoint.tools.GeneralException; 035 036/** 037 * Utilities for matching objects using IIQ APIs 038 */ 039public class MatchUtilities extends AbstractBaseUtility { 040 041 /** 042 * Basic constructor 043 * @param c The IIQ context 044 */ 045 public MatchUtilities(SailPointContext c) { 046 super(c); 047 } 048 049 /** 050 * Returns the list of matching Link objects by application for this role. Each profile 051 * may match more than one Link. Each Link will only be returned once, even if it matches 052 * multiple profiles. 053 * 054 * Use {@link #identityMatchesSimpleProfiles(Bundle, Identity)} first to determine an overall 055 * match and then this method to extract the matches. 056 * 057 * The result will be empty if the role has no profiles or is null. 058 * 059 * @param role The role from which profiles should be checked 060 * @param identity The identity from which to extract account details 061 * @return A map of Application Name -> List of matching Links 062 * @throws GeneralException if any lookup failures occur 063 */ 064 public Map<String, List<Link>> findMatchingLinksByApplication(Bundle role, Identity identity) throws GeneralException { 065 Map<String, List<Link>> results = new HashMap<>(); 066 // Don't match an empty role 067 if (role == null || role.getProfiles() == null || role.getProfiles().isEmpty()) { 068 return results; 069 } 070 IdentityService ids = new IdentityService(context); 071 for(Profile profile : role.getProfiles()) { 072 List<Link> links = new ArrayList<>(); 073 if (profile.getApplication() != null) { 074 links = ids.getLinks(identity, profile.getApplication()); 075 } 076 if (links != null && links.size() > 0) { 077 final String application = profile.getApplication().getName(); 078 for(Link link : links) { 079 boolean matched = linkMatchesProfile(profile, link); 080 if (matched) { 081 if (!results.containsKey(application)) { 082 results.put(application, new ArrayList<>()); 083 } 084 if (!results.get(application).contains(link)) { 085 results.get(application).add(link); 086 } 087 } 088 } 089 } 090 } 091 return results; 092 } 093 094 /** 095 * Performs some reflection digging into the Sailpoint API to transform a standard 096 * {@link Filter} into a Hibernate HQL query. 097 * @param target The target filter to transform 098 * @param targetClass The target class (determines the queried tables) 099 * @param columns The columns to return 100 * @return The HQL query 101 * @throws GeneralException if any failures occur accessing the internal APIs 102 */ 103 public String getFilterHQL(Filter target, Class<? extends SailPointObject> targetClass, String... columns) throws GeneralException { 104 SailPointContext privateSession = SailPointFactory.createPrivateContext(); 105 try { 106 HibernatePersistenceManager manager = HibernatePersistenceManager.getHibernatePersistenceManager(privateSession); 107 Method visitHQLFilter = manager.getClass().getDeclaredMethod("visitHQLFilter", Class.class, QueryOptions.class, List.class); 108 visitHQLFilter.setAccessible(true); 109 try { 110 manager.startTransaction(); 111 try { 112 QueryOptions qo = new QueryOptions(); 113 qo.addFilter(target); 114 List<String> cols = new ArrayList<>(Arrays.asList(columns)); 115 BaseFilterVisitor visitor = (BaseFilterVisitor) visitHQLFilter.invoke(manager, targetClass, qo, cols); 116 Method getQueryString = visitor.getClass().getDeclaredMethod("getQueryString"); 117 getQueryString.setAccessible(true); 118 return (String)getQueryString.invoke(visitor); 119 } catch(InvocationTargetException e) { 120 if (e.getTargetException() instanceof GeneralException) { 121 throw (GeneralException)e.getTargetException(); 122 } else { 123 throw new GeneralException(e.getTargetException()); 124 } 125 } catch (IllegalAccessException | IllegalArgumentException e) { 126 throw new GeneralException(e); 127 } finally { 128 manager.commitTransaction(); 129 } 130 } finally { 131 visitHQLFilter.setAccessible(false); 132 } 133 } catch (NoSuchMethodException | SecurityException e1) { 134 throw new GeneralException(e1); 135 } finally { 136 SailPointFactory.releasePrivateContext(privateSession); 137 } 138 } 139 140 /** 141 * Returns true if the given identity has accounts matching all of the profiles on the given role 142 * @param role The role to check 143 * @param identity The identity whose links to check against the role 144 * @return True if the link matches all of the Bundle profiles 145 * @throws GeneralException if any failures occur 146 */ 147 public boolean identityMatchesSimpleProfiles(Bundle role, Identity identity) throws GeneralException { 148 Objects.requireNonNull(identity, "Cannot match profiles against a null Identity"); 149 int matchedCount = 0; 150 // Don't match an empty role 151 if (role == null || role.getProfiles() == null || role.getProfiles().isEmpty()) { 152 return false; 153 } 154 IdentityService ids = new IdentityService(context); 155 for(Profile profile : role.getProfiles()) { 156 List<Link> links = new ArrayList<>(); 157 if (profile.getApplication() != null) { 158 links = ids.getLinks(identity, profile.getApplication()); 159 } 160 if (links != null && links.size() > 0) { 161 // Check each Link of the appropriate type against this Profile. The Profile may have multiple filters and they must ALL match to count. 162 for(Link link : links) { 163 boolean matched = linkMatchesProfile(profile, link); 164 // If the link matched all filters, count it as a match toward this Profile 165 // and don't check any further Links. 166 if (matched) { 167 matchedCount++; 168 break; 169 } 170 } 171 } 172 } 173 if (role.isOrProfiles() && matchedCount > 1) { 174 return true; 175 } else if (matchedCount == role.getProfiles().size()) { 176 return true; 177 } 178 return false; 179 } 180 181 /** 182 * Returns true if the given identity would be a member of the given IdentityFilter (e.g. used for searching) 183 * @param test The identity to test 184 * @param target The filter to check 185 * @param requestParameters Any parameters required by the filter script, if one exists 186 * @return true if the identity matches the filter criteria 187 * @throws GeneralException if any match failure occurs 188 */ 189 public boolean identitySelectorMatches(Identity test, IdentityFilter target, Map<String, Object> requestParameters) throws GeneralException { 190 QueryOptions qo = target.buildQuery(requestParameters, context); 191 qo.addFilter(Filter.eq("Identity.id", test.getId())); 192 return (context.countObjects(Identity.class, qo) > 0); 193 } 194 195 /** 196 * Returns true if the given identity would be a member of the given IdentityFilter (e.g. used for searching) 197 * @param test The identity to test 198 * @param selectorName The Identity Selector to query 199 * @param requestParameters Any parameters required by the filter script, if one exists 200 * @return true if the identity matches the filter criteria 201 * @throws GeneralException if any match failure occurs 202 */ 203 public boolean identitySelectorMatches(Identity test, String selectorName, Map<String, Object> requestParameters) throws GeneralException { 204 IdentityService ids = new IdentityService(context); 205 Map<String, Object> suggestParams = new HashMap<String, Object>(); 206 suggestParams.putAll(requestParameters); 207 suggestParams.put("suggestId", selectorName); 208 QueryOptions qo = ids.getIdentitySuggestQueryOptions(suggestParams, context.getObject(Identity.class, context.getUserName())); 209 qo.addFilter(Filter.eq("Identity.id", test.getId())); 210 return (context.countObjects(Identity.class, qo) > 0); 211 } 212 213 /** 214 * Returns true if the given Profile matches the given Link via {@link MapMatcher}. 215 * @param profile The profile 216 * @param link The link 217 * @return true if the link matches the profile 218 * @throws GeneralException if any failures occur during matching (should never happen) 219 */ 220 public boolean linkMatchesProfile(Profile profile, Link link) throws GeneralException { 221 boolean matched = true; 222 List<Filter> filters = profile.getConstraints(); 223 if (filters != null) { 224 for (Filter filter : filters) { 225 MapMatcher matcher = new MapMatcher(filter); 226 if (!matcher.matches(link.getAttributes())) { 227 matched = false; 228 break; 229 } 230 } 231 } 232 return matched; 233 } 234 235 /** 236 * Returns true if the given link matches the profiles on the given role 237 * @param role The role to check 238 * @param link The link to check against the role 239 * @return True if the link matches all of the Bundle profiles 240 * @throws GeneralException if any failures occur 241 */ 242 public boolean linkMatchesSimpleProfiles(Bundle role, Link link) throws GeneralException { 243 int matchedCount = 0; 244 // Don't match an empty role 245 if (role.getProfiles() == null || role.getProfiles().isEmpty()) { 246 return false; 247 } 248 for(Profile profile : role.getProfiles()) { 249 boolean matched = true; 250 List<Filter> filters = profile.getConstraints(); 251 if (filters != null) { 252 for (Filter filter : filters) { 253 MapMatcher matcher = new MapMatcher(filter); 254 if (!matcher.matches(link.getAttributes())) { 255 matched = false; 256 } 257 } 258 } 259 if (matched) { 260 matchedCount++; 261 } 262 } 263 if (role.isOrProfiles() && matchedCount > 0) { 264 return true; 265 } else if (matchedCount == role.getProfiles().size()) { 266 return true; 267 } 268 return false; 269 } 270 271 /** 272 * Returns true if the given Identity would be a member of the target Bundle, either by assignment or detection 273 * @param target The role to check 274 * @param test The identity to test 275 * @return true if the identity matches the role criteria 276 * @throws GeneralException if any match failure occurs 277 */ 278 public boolean matches(Bundle target, Identity test) throws GeneralException { 279 return matches(test, target); 280 } 281 282 /** 283 * Returns true if the given Filter would by matched by the given Identity 284 * @param target The filter to run 285 * @param test The Identity to test against the filter 286 * @return true if the identity matches the Filter 287 * @throws GeneralException if a query exception occurs 288 */ 289 public boolean matches(Filter target, Identity test) throws GeneralException { 290 return matches(test, target); 291 } 292 293 /** 294 * Returns true if the given Identity would be a member of the target Bundle, either by assignment or detection 295 * @param target The identity to test 296 * @param role The bundle to test 297 * @return true if the identity matches the role criteria 298 * @throws GeneralException if any match failure occurs 299 */ 300 public boolean matches(Identity target, Bundle role) throws GeneralException { 301 boolean isAssignmentRole = true; 302 if (role.getSelector() == null) { 303 isAssignmentRole = false; 304 if (role.getProfiles() == null || role.getProfiles().isEmpty()) { 305 throw new IllegalArgumentException("Role " + role.getName() + " has no selector and no profiles"); 306 } 307 } 308 if (isAssignmentRole) { 309 // Business role needs assignment selector 310 Matchmaker matcher = new Matchmaker(context); 311 matcher.setArgument("iiqNoCompilationCache", "true"); 312 matcher.setArgument("roleName", role.getName()); 313 matcher.setArgument("identity", target); 314 return matcher.isMatch(role.getSelector(), target); 315 } else { 316 EntitlementCorrelator correlator = new EntitlementCorrelator(context); 317 correlator.setDoRoleAssignment(true); 318 correlator.setNoPersistentIdentity(true); 319 correlator.analyzeIdentity(target); 320 321 Identity dummyTarget = new Identity(); 322 correlator.saveDetectionAnalysis(dummyTarget); 323 324 return dummyTarget.getDetectedRoles().stream().map(SailPointObject::getName).collect(Collectors.toList()).contains(role.getName()); 325 } 326 } 327 328 /** 329 * Returns true if the given identity would match the target filter 330 * @param target The target identity 331 * @param filter The compound filter to check 332 * @return True if the identity matches the filer 333 * @throws GeneralException if any match failure occurs 334 */ 335 public boolean matches(Identity target, CompoundFilter filter) throws GeneralException { 336 IdentitySelector selector = new IdentitySelector(); 337 selector.setFilter(filter); 338 return matches(target, selector); 339 } 340 341 /** 342 * Returns true if the given identity matches the given Dynamic Scope 343 * @param target The target to check 344 * @param scope The scope to match against 345 * @return true if the identity matches the dynamic scope filter 346 * @throws GeneralException if any match failure occurs 347 */ 348 public boolean matches(Identity target, DynamicScope scope) throws GeneralException { 349 DynamicScopeMatchmaker matchmaker = new DynamicScopeMatchmaker(context); 350 return matchmaker.isMatch(scope, target); 351 } 352 353 /** 354 * Returns true if the given identity would match the target filter 355 * @param target The target identity 356 * @param filter The compound filter to check 357 * @return True if the identity matches the filer 358 * @throws GeneralException if any match failure occurs 359 */ 360 public boolean matches(Identity target, Filter filter) throws GeneralException { 361 CompoundFilter compound = new CompoundFilter(); 362 compound.setFilter(filter); 363 return matches(target, compound); 364 } 365 366 /** 367 * Returns true if the given identity would match the target population's filter 368 * @param target The target identity 369 * @param population The population to check 370 * @return True if the identity matches the filer 371 * @throws GeneralException if any match failure occurs 372 */ 373 public boolean matches(Identity target, GroupDefinition population) throws GeneralException { 374 if (population.getFilter() == null) { 375 throw new IllegalArgumentException("Population " + population.getName() + " has no filter defined"); 376 } 377 IdentitySelector selector = new IdentitySelector(); 378 selector.setPopulation(population); 379 return matches(target, selector); 380 } 381 382 /** 383 * Returns true if the given identity would match the target filter 384 * @param target The target identity 385 * @param selector The selector / filter to check 386 * @return True if the identity matches the filer 387 * @throws GeneralException if any match failure occurs 388 */ 389 public boolean matches(Identity target, IdentitySelector selector) throws GeneralException { 390 Matchmaker matcher = new Matchmaker(context); 391 matcher.setArgument("iiqNoCompilationCache", "true"); 392 matcher.setArgument("identity", target); 393 return matcher.isMatch(selector, target); 394 } 395 396 /** 397 * Returns true if the given identity would match the target filter 398 * @param target The target identity 399 * @param filter The filter string to compile and check 400 * @return True if the identity matches the filer 401 * @throws GeneralException if any match failure occurs 402 */ 403 public boolean matches(Identity target, String filter) throws GeneralException { 404 Filter f = Filter.compile(filter); 405 return matches(target, f); 406 } 407 408 /** 409 * Returns true if the given identity would match the target filter 410 * @param target The target selector 411 * @param test The identity to examine 412 * @return True if the identity matches the filer 413 * @throws GeneralException if any match failure occurs 414 */ 415 public boolean matches(IdentitySelector target, Identity test) throws GeneralException { 416 return matches(test, target); 417 } 418 419}