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