001package com.identityworksllc.iiq.common; 002 003import com.identityworksllc.iiq.common.annotation.CoreStable; 004import com.identityworksllc.iiq.common.annotation.InProgress; 005import sailpoint.api.IdentityService; 006import sailpoint.api.SailPointContext; 007import sailpoint.object.*; 008import sailpoint.tools.GeneralException; 009import sailpoint.tools.ObjectNotFoundException; 010import sailpoint.tools.Util; 011 012import javax.validation.constraints.Null; 013import java.util.ArrayList; 014import java.util.Collection; 015import java.util.HashMap; 016import java.util.List; 017import java.util.Map; 018import java.util.Objects; 019import java.util.Optional; 020 021/** 022 * A utility class for efficiently reading various types of information from 023 * Link objects. This class supplements IdentityService by providing additional 024 * methods for retrieving attributes of various types. 025 * 026 * This is very common logic in virtually all IIQ instances, and this will prevent 027 * us from having to reimplement it for both Beanshell and Java every time. It 028 * should also increase efficiency by being in compiled Java and not Beanshell. 029 * 030 * There are essentially two modes of operation, depending on your purposes. If 031 * you need to pre-load all Identity Links prior to running an operation, set the 032 * forceLoad flag to true using {@link #setForceLoad(boolean)}. If you do not do 033 * this, this class will echo the logic used by {@link IdentityService#getLinks(Identity, Application)}. 034 * 035 * You will generally NOT want to set forceLoad=true unless you need to repeatedly 036 * query the same Links from the Identity. The most notable example is when you 037 * are reading values from Links in more than a handful of IdentityAttribute rules. 038 * 039 * In all cases, the get-attribute methods take the 'failOnMultiple' flag into 040 * account. If the flag is false, as is default, the value from the newest Link 041 * will be retrieved. 042 */ 043@CoreStable 044public class IdentityLinkUtil { 045 046 /** 047 * The Sailpoint context 048 */ 049 private final SailPointContext context; 050 /** 051 * If true, the various get-attribute methods will fail if the user has more 052 * than one of the same type. 053 */ 054 private boolean failOnMultiple; 055 /** 056 * If true, the identity's Links will be forcibly loaded by calling load() 057 * on the whole collection before running any operation. This will make 058 * subsequent operations on the same object in the same session potentially 059 * faster. You also will want to use this option in an Identity Attribute 060 * rule, as those may be invoked before the Identity or Link is persisted. 061 */ 062 private boolean forceLoad; 063 /** 064 * The global link filter, to be applied to any queries for a Link by application 065 */ 066 private Filter globalLinkFilter; 067 /** 068 * The Identity associated with this utility 069 */ 070 private final Identity identity; 071 /** 072 * Identity Link utility constructor 073 * @param context The Sailpoint context 074 * @param identity the Identity 075 */ 076 public IdentityLinkUtil(SailPointContext context, Identity identity) { 077 this(context, identity, null); 078 } 079 /** 080 * Identity Link utility constructor 081 * @param context The Sailpoint context 082 * @param identity the Identity 083 */ 084 public IdentityLinkUtil(SailPointContext context, Identity identity, Filter globalLinkFilter) { 085 this.context = Objects.requireNonNull(context); 086 this.identity = Objects.requireNonNull(identity); 087 this.forceLoad = false; 088 this.failOnMultiple = false; 089 this.globalLinkFilter = globalLinkFilter; 090 } 091 092 /** 093 * Finds a unique Link by Native ID and Application, returning a non-null Optional 094 * @param context The context for querying 095 * @param applicationName The application name 096 * @param nativeIdentity The native ID 097 * @return If no matches, an empty Optional. If one match, an Optional containing the Link 098 * @throws GeneralException if there is a query failure 099 * @throws TooManyResultsException if more than one Link matches the criteria 100 */ 101 public static Optional<Link> findUniqueLink(SailPointContext context, String applicationName, String nativeIdentity) throws GeneralException { 102 QueryOptions qo = new QueryOptions(); 103 Filter theFilter = getLinkFilter(applicationName, nativeIdentity); 104 105 qo.addFilter(theFilter); 106 107 List<Link> links = context.getObjects(Link.class, qo); 108 if (links == null || links.size() == 0) { 109 return Optional.empty(); 110 } else if (links.size() == 1) { 111 return Optional.of(links.get(0)); 112 } else { 113 throw new TooManyResultsException(Link.class, theFilter.getExpression(true), links.size()); 114 } 115 } 116 117 /** 118 * Returns a Filter object for a Link 119 * @param applicationName The application name 120 * @param nativeIdentity The native ID 121 * @return the resulting Filter 122 */ 123 public static Filter getLinkFilter(String applicationName, String nativeIdentity) { 124 return Filter.and( 125 Filter.eq("application.name", applicationName), 126 Filter.eq("nativeIdentity", nativeIdentity) 127 ); 128 } 129 130 /** 131 * Gets a unique Link by Native ID and Application or else throws an exception 132 * @param context The context for querying 133 * @param applicationName The application name 134 * @param nativeIdentity The native ID 135 * @return Null if no matches, a single Link if there is a match 136 * @throws GeneralException if there is a query failure 137 * @throws TooManyResultsException if more than one Link matches the criteria 138 */ 139 public static Link getUniqueLink(SailPointContext context, String applicationName, String nativeIdentity) throws GeneralException { 140 QueryOptions qo = new QueryOptions(); 141 Filter theFilter = getLinkFilter(applicationName, nativeIdentity); 142 143 qo.addFilter(theFilter); 144 145 List<Link> links = context.getObjects(Link.class, qo); 146 if (links == null || links.size() == 0) { 147 return null; 148 } else if (links.size() == 1) { 149 return links.get(0); 150 } else { 151 throw new TooManyResultsException(Link.class, theFilter.getExpression(true), links.size()); 152 } 153 } 154 155 /** 156 * Iterates over the list of Links on this Identity and loads them all 157 */ 158 private void checkLoaded() { 159 Iterable<Link> links = Util.safeIterable(identity.getLinks()); 160 for (Link l : links) { 161 l.load(); 162 } 163 } 164 165 /** 166 * Retrieves a managed attribute for the given IdentityEntitlement 167 * @param ie The IdentityEntitlement 168 * @return The associated managed attribute, or an empty optional 169 * @throws GeneralException If the query fails for some reason 170 * @throws TooManyResultsException If the entitlement matches more than 1 managed attribute 171 */ 172 public Optional<ManagedAttribute> findManagedAttribute(IdentityEntitlement ie) throws GeneralException { 173 if (ie == null) { 174 throw new NullPointerException("IdentityEntitlement"); 175 } 176 QueryOptions qo = new QueryOptions(); 177 qo.addFilter(Filter.eq("application.name", ie.getApplication().getName())); 178 qo.addFilter(Filter.eq("attribute", ie.getName())); 179 qo.addFilter(Filter.eq("value", ie.getValue())); 180 181 List<ManagedAttribute> managedAttributes = context.getObjects(ManagedAttribute.class, qo); 182 183 if (managedAttributes == null || managedAttributes.size() == 0) { 184 return Optional.empty(); 185 } else if (managedAttributes.size() == 1) { 186 return Optional.of(managedAttributes.get(0)); 187 } else { 188 throw new TooManyResultsException(ManagedAttribute.class, qo.toString(), managedAttributes.size()); 189 } 190 } 191 192 /** 193 * Retrieves all ManagedAttributes associated with the given Link 194 * @param link the Link to check 195 * @return A map from field name to a list of ManagedAttribute objects 196 * @throws GeneralException If the query fails for some reason 197 * @throws TooManyResultsException If the entitlement matches more than 1 managed attribute 198 */ 199 public Map<String, List<ManagedAttribute>> findManagedAttributes(Link link) throws GeneralException { 200 if (link == null || link.getAttributes() == null) { 201 throw new NullPointerException("Link or Link.attributes is null"); 202 } 203 204 Map<String, List<ManagedAttribute>> result = new HashMap<>(); 205 206 String appName = link.getApplicationName(); 207 208 @SuppressWarnings("unchecked") 209 Attributes<String, Object> entitlementAttributes = link.getEntitlementAttributes(); 210 211 for(String fieldName : entitlementAttributes.getKeys()) { 212 List<String> values = Util.otol(entitlementAttributes.get(fieldName)); 213 result.put(fieldName, new ArrayList<>()); 214 215 for(String value : values) { 216 QueryOptions qo = new QueryOptions(); 217 qo.addFilter(Filter.eq("application.name", appName)); 218 qo.addFilter(Filter.eq("attribute", fieldName)); 219 qo.addFilter(Filter.eq("value", value)); 220 221 List<ManagedAttribute> managedAttributes = context.getObjects(ManagedAttribute.class, qo); 222 223 if (managedAttributes != null && managedAttributes.size() > 0) { 224 if (managedAttributes.size() == 1) { 225 result.get(fieldName).add(managedAttributes.get(0)); 226 } else { 227 throw new TooManyResultsException(ManagedAttribute.class, qo.toString(), managedAttributes.size()); 228 } 229 } // else { no match, ignore it } 230 } 231 } 232 233 return result; 234 } 235 236 /** 237 * Gets the applied (possibly null) global link filter 238 * @return The applied global link filter 239 */ 240 public Filter getGlobalLinkFilter() { 241 return globalLinkFilter; 242 } 243 244 /** 245 * Gets the Link from the Identity by native identity 246 * @param application The application type of the Link 247 * @param nativeIdentity The native identity of the Link 248 * @return The Link 249 * @throws GeneralException if any failures occur 250 */ 251 public Link getLinkByNativeIdentity(Application application, String nativeIdentity) throws GeneralException { 252 if (forceLoad) { 253 checkLoaded(); 254 } 255 256 IdentityService ids = new IdentityService(context); 257 return ids.getLink(identity, application, null, nativeIdentity); 258 } 259 260 /** 261 * @see #getLinkByNativeIdentity(Application, String) 262 */ 263 public Link getLinkByNativeIdentity(String applicationName, String nativeIdentity) throws GeneralException { 264 Application application = context.getObject(Application.class, applicationName); 265 266 if (application == null) { 267 throw new ObjectNotFoundException(Application.class, applicationName); 268 } 269 270 return getLinkByNativeIdentity(application, nativeIdentity); 271 } 272 273 /** 274 * @see #getLinksByApplication(Application, Filter) 275 */ 276 public List<Link> getLinksByApplication(Application application) throws GeneralException { 277 return getLinksByApplication(application, null); 278 } 279 280 /** 281 * Gets the list of Links of the given application type, applying the given optional 282 * filter to the links. If a filter is present, only Links matching the filter will be 283 * returned. 284 * 285 * @param application The application object 286 * @param linkFilter The filter object, optional 287 * @return A non-null list of links (optionally filtered) on this user of the given application type 288 * @throws GeneralException if any failures occur 289 */ 290 public List<Link> getLinksByApplication(Application application, Filter linkFilter) throws GeneralException { 291 if (forceLoad) { 292 checkLoaded(); 293 } 294 295 IdentityService ids = new IdentityService(context); 296 List<Link> links = ids.getLinks(identity, application); 297 298 if (links == null) { 299 links = new ArrayList<>(); 300 } else { 301 // Ensure that the list is mutable and detached from the Identity 302 links = new ArrayList<>(links); 303 } 304 Filter finalFilter = null; 305 306 if (this.globalLinkFilter != null && linkFilter != null) { 307 finalFilter = Filter.and(this.globalLinkFilter, linkFilter); 308 } else if (this.globalLinkFilter != null) { 309 finalFilter = this.globalLinkFilter; 310 } else if (linkFilter != null) { 311 finalFilter = linkFilter; 312 } 313 314 if (finalFilter != null) { 315 List<Link> newList = new ArrayList<>(); 316 HybridObjectMatcher matcher = new HybridObjectMatcher(context, finalFilter); 317 for(Link l : links) { 318 if (matcher.matches(l)) { 319 newList.add(l); 320 } 321 } 322 links = newList; 323 } 324 325 return links; 326 } 327 328 /** 329 * @see #getLinksByApplication(Application, Filter) 330 */ 331 public List<Link> getLinksByApplication(String applicationName) throws GeneralException { 332 Application application = context.getObject(Application.class, applicationName); 333 334 if (application == null) { 335 throw new ObjectNotFoundException(Application.class, applicationName); 336 } 337 338 return getLinksByApplication(application); 339 } 340 341 /** 342 * @see #getLinksByApplication(Application, Filter) 343 */ 344 public List<Link> getLinksByApplication(String applicationName, Filter linkFilter) throws GeneralException { 345 Application application = context.getObject(Application.class, applicationName); 346 347 if (application == null) { 348 throw new ObjectNotFoundException(Application.class, applicationName); 349 } 350 351 return getLinksByApplication(application, linkFilter); 352 } 353 354 /** 355 * @see #getMultiValueLinkAttribute(Application, String, Filter) 356 */ 357 public List<String> getMultiValueLinkAttribute(String applicationName, String attributeName) throws GeneralException { 358 return getMultiValueLinkAttribute(applicationName, attributeName, null); 359 } 360 361 /** 362 * @see #getMultiValueLinkAttribute(Application, String, Filter) 363 */ 364 public List<String> getMultiValueLinkAttribute(String applicationName, String attributeName, Filter linkFilter) throws GeneralException { 365 Application application = context.getObject(Application.class, applicationName); 366 367 if (application == null) { 368 throw new ObjectNotFoundException(Application.class, applicationName); 369 } 370 371 return getMultiValueLinkAttribute(application, attributeName, linkFilter); 372 } 373 374 /** 375 * Gets the value of a multi-valued attribute from one Link of the given type 376 * belonging to this Identity. The actual type of the attribute doesn't matter. 377 * A CSV single-valued String will be converted to a List here. 378 * 379 * @param application The application type of the Links 380 * @param attributeName The attribute name to grab 381 * @param linkFilter The Link filter, optional 382 * @return The value of the attribute, or null 383 * @throws GeneralException if any errors occur 384 */ 385 public List<String> getMultiValueLinkAttribute(Application application, String attributeName, Filter linkFilter) throws GeneralException { 386 List<Link> links = getLinksByApplication(application, linkFilter); 387 388 if (Util.isEmpty(links)) { 389 return null; 390 } else if (links.size() == 1) { 391 Object value = links.get(0).getAttribute(attributeName); 392 return Util.otol(value); 393 } else { 394 if (failOnMultiple) { 395 throw new GeneralException("Too many accounts of type " + application.getName()); 396 } else { 397 SailPointObjectDateSorter.sort(links); 398 Object value = links.get(0).getAttribute(attributeName); 399 return Util.otol(value); 400 } 401 } 402 } 403 404 /** 405 * @see #getMultiValueLinkAttribute(Application, String, Filter) 406 */ 407 public List<String> getMultiValueLinkAttribute(Application application, String attributeName) throws GeneralException { 408 return getMultiValueLinkAttribute(application, attributeName, null); 409 } 410 411 /** 412 * @see #getSingleValueLinkAttribute(Application, String, Filter) 413 */ 414 public String getSingleValueLinkAttribute(String applicationName, String attributeName) throws GeneralException { 415 Application application = context.getObject(Application.class, applicationName); 416 417 if (application == null) { 418 throw new ObjectNotFoundException(Application.class, applicationName); 419 } 420 421 return getSingleValueLinkAttribute(application, attributeName, null); 422 } 423 424 /** 425 * @see #getSingleValueLinkAttribute(Application, String, Filter) 426 */ 427 public String getSingleValueLinkAttribute(String applicationName, String attributeName, Filter linkFilter) throws GeneralException { 428 Application application = context.getObject(Application.class, applicationName); 429 430 if (application == null) { 431 throw new ObjectNotFoundException(Application.class, applicationName); 432 } 433 434 return getSingleValueLinkAttribute(application, attributeName, linkFilter); 435 } 436 437 /** 438 * @see #getSingleValueLinkAttribute(Application, String, Filter) 439 */ 440 public String getSingleValueLinkAttribute(Application application, String attributeName) throws GeneralException { 441 return getSingleValueLinkAttribute(application, attributeName, null); 442 } 443 444 /** 445 * Gets the value of a single-valued attribute from one Link of the given type 446 * belonging to this Identity. 447 * 448 * @param application The application type of the Links 449 * @param attributeName The attribute name to grab 450 * @param linkFilter The Link filter, optional 451 * @return The value of the attribute, or null 452 * @throws GeneralException if any errors occur 453 */ 454 public String getSingleValueLinkAttribute(Application application, String attributeName, Filter linkFilter) throws GeneralException { 455 List<Link> links = getLinksByApplication(application, linkFilter); 456 457 if (Util.isEmpty(links)) { 458 return null; 459 } else if (links.size() == 1) { 460 Object value = links.get(0).getAttribute(attributeName); 461 return Util.otoa(value); 462 } else { 463 if (failOnMultiple) { 464 throw new GeneralException("Too many accounts of type " + application.getName()); 465 } else { 466 SailPointObjectDateSorter.sort(links); 467 Object value = links.get(0).getAttribute(attributeName); 468 return Util.otoa(value); 469 } 470 } 471 } 472 473 /** 474 * A shortcut that returns true if the Identity has at least one Link of the given 475 * application type. 476 * 477 * @param applicationName The application name to check 478 * @return True if the Identity has at least one Link of the given application type 479 * @throws GeneralException if any failures occur 480 */ 481 @InProgress 482 public boolean hasLinkByApplication(String applicationName) throws GeneralException { 483 List<Link> links = getLinksByApplication(applicationName); 484 return links != null && !links.isEmpty(); 485 } 486 487 /** 488 * Returns true if the class is set to fail on multiple Links of the same type 489 * @see #failOnMultiple 490 */ 491 public boolean isFailOnMultiple() { 492 return failOnMultiple; 493 } 494 495 /** 496 * Returns true if you want to force-load all Links on the Identity using {@link Identity#getLinks()}, 497 * rather than using {@link IdentityService} 498 * @see #forceLoad 499 */ 500 public boolean isForceLoad() { 501 return forceLoad; 502 } 503 504 /** 505 * @see #mergeLinkAttributes(Application, String, Filter) 506 */ 507 public List<String> mergeLinkAttributes(String applicationName, String attributeName) throws GeneralException { 508 Application application = context.getObject(Application.class, applicationName); 509 510 if (application == null) { 511 throw new ObjectNotFoundException(Application.class, applicationName); 512 } 513 514 return mergeLinkAttributes(application, attributeName, null); 515 } 516 517 /** 518 * @see #mergeLinkAttributes(Application, String, Filter) 519 */ 520 public List<String> mergeLinkAttributes(String applicationName, String attributeName, Filter linkFilter) throws GeneralException { 521 Application application = context.getObject(Application.class, applicationName); 522 523 if (application == null) { 524 throw new ObjectNotFoundException(Application.class, applicationName); 525 } 526 527 return mergeLinkAttributes(application, attributeName, linkFilter); 528 } 529 530 /** 531 * Extracts the named attribute from each Link of the given application and adds 532 * all values from each Link into a common List. 533 * 534 * @param application The application to query 535 * @param attributeName The attribute name to query 536 * @param linkFilter The link filter, optional 537 * @return The merged set of attributes from each application 538 * @throws GeneralException if any failures occur 539 */ 540 public List<String> mergeLinkAttributes(Application application, String attributeName, Filter linkFilter) throws GeneralException { 541 List<Link> links = getLinksByApplication(application, linkFilter); 542 543 boolean isMultiValued = false; 544 545 Schema accountSchema = application.getAccountSchema(); 546 if (accountSchema != null) { 547 AttributeDefinition attributeDefinition = accountSchema.getAttributeDefinition(attributeName); 548 if (attributeDefinition != null) { 549 isMultiValued = attributeDefinition.isMultiValued(); 550 } 551 } 552 553 List<String> values = new ArrayList<>(); 554 for(Link l : links) { 555 Object value = l.getAttribute(attributeName); 556 if (isMultiValued) { 557 value = Util.otol(value); 558 } else { 559 value = Util.otoa(value); 560 } 561 562 if (value instanceof String) { 563 values.add((String)value); 564 } else if (value instanceof Collection) { 565 @SuppressWarnings("unchecked") 566 Collection<String> c = (Collection<String>)value; 567 values.addAll(c); 568 } 569 } 570 return values; 571 } 572 573 /** 574 * If true, and the Identity has more than one (post-filter) Link of a given 575 * Application type, the get-attribute methods will throw an exception. 576 * 577 * @param failOnMultiple True if we should fail on multiple accounts 578 */ 579 public void setFailOnMultiple(boolean failOnMultiple) { 580 this.failOnMultiple = failOnMultiple; 581 } 582 583 /** 584 * If true, the Identity's `links` container will be populated before searching for 585 * items. This will make the IdentityService faster in some circumstances, notably 586 * repeated queries of links in Identity Attributes. 587 * 588 * @param forceLoad True if we should always load the Link objects 589 */ 590 public void setForceLoad(boolean forceLoad) { 591 this.forceLoad = forceLoad; 592 } 593 594 /** 595 * Sets a global link filter, allowing use of a constant 596 * @param globalLinkFilter The filter to apply to any operation 597 */ 598 public void setGlobalLinkFilter(Filter globalLinkFilter) { 599 this.globalLinkFilter = globalLinkFilter; 600 } 601}