001package com.identityworksllc.iiq.common; 002 003import org.apache.commons.logging.Log; 004import org.apache.commons.logging.LogFactory; 005import sailpoint.api.Differencer; 006import sailpoint.api.SailPointContext; 007import sailpoint.object.*; 008import sailpoint.tools.GeneralException; 009import sailpoint.tools.Pair; 010import sailpoint.tools.Util; 011 012import java.util.*; 013import java.util.function.Consumer; 014import java.util.function.Function; 015import java.util.function.Predicate; 016import java.util.stream.Collectors; 017 018/** 019 * An API to reimplement the Differencer to increase its reliability with various 020 * Sailpoint objects. 021 * 022 * Most notably: 023 * 024 * (1) LinkSnapshots will be compared correctly. That is, if the user has more than one 025 * account on the same Application, the correct LinkSnapshot will be returned, rather 026 * than simply returning the first one. Only if there are no matches by Native Identity 027 * will the first LinkSnapshot be returned. 028 * 029 * (2) The Sameness class will be used to compare changes. This will account for a few 030 * differences in type and order that are not accounted for out-of-box. 031 * 032 * (3) Changes solely in String case will be ignored in most cases. 033 * 034 * (4) Old and new values will be sorted, for easier comparison. 035 * 036 * TODO: Policy violation differences 037 * TODO: Javadocs! 038 */ 039public class BetterDifferencer { 040 041 /** 042 * A container for holding a pair of links judged by this tool to be the same 043 * Link in two different snapshot contexts 044 */ 045 private static class LinkPair { 046 private LinkSnapshot ls1; 047 private LinkSnapshot ls2; 048 049 public LinkPair() { 050 /* Empty */ 051 } 052 053 public LinkPair(LinkSnapshot ls1, LinkSnapshot ls2) { 054 this.ls1 = ls1; 055 this.ls2 = ls2; 056 } 057 058 public LinkSnapshot getLs1() { 059 return ls1; 060 } 061 062 public LinkSnapshot getLs2() { 063 return ls2; 064 } 065 066 public void setLs1(LinkSnapshot ls1) { 067 this.ls1 = ls1; 068 } 069 070 public void setLs2(LinkSnapshot ls2) { 071 this.ls2 = ls2; 072 } 073 } 074 075 /** 076 * A set of applications where the comparison ought to be case-insensitive 077 */ 078 private final Set<String> caseInsensitiveApplications; 079 080 /** 081 * A set of fields on which the comparison should be done case-insensitively 082 */ 083 private final Set<String> caseInsensitiveFields; 084 private final SailPointContext context; 085 086 /** 087 * If true, we should guess when there is a plausible account rename. This is 088 * only possible if the user had exactly one account before and exactly one 089 * account after. 090 */ 091 private boolean guessRenames; 092 private final Log log; 093 /** 094 * A map from the old to new name of a renamed application 095 */ 096 private final Map<String, String> renamedApplications; 097 098 /** 099 * Constructs a new BetterDifferencer with the given IIQ context 100 * @param context The IIQ context to use for lookups 101 */ 102 public BetterDifferencer(SailPointContext context) { 103 this.log = LogFactory.getLog(this.getClass()); 104 this.context = Objects.requireNonNull(context); 105 this.caseInsensitiveFields = new HashSet<>(); 106 this.caseInsensitiveApplications = new HashSet<>(); 107 this.renamedApplications = new HashMap<>(); 108 } 109 110 /** 111 * Adds the added and removed values to the Difference object by comparing the old 112 * and new values. 113 * 114 * @param difference The Difference object to populate 115 * @param oldValue The old value, which may be a collection or a single-valued object 116 * @param newValue The new value, which may be a collection or a single-valued object 117 */ 118 private void addAddedRemovedValues(Difference difference, Object oldValue, Object newValue) { 119 List<Object> oldCollection = new ArrayList<>(); 120 List<Object> newCollection = new ArrayList<>(); 121 122 if (oldValue instanceof Collection) { 123 oldCollection.addAll((Collection<?>)oldValue); 124 } else if (oldValue != null) { 125 oldCollection.add(oldValue); 126 } 127 128 if (newValue instanceof Collection) { 129 newCollection.addAll((Collection<?>)newValue); 130 } else if (newValue != null) { 131 newCollection.add(newValue); 132 } 133 134 List<String> removedValues = oldCollection.stream().filter(o -> !Utilities.caseInsensitiveContains(newCollection, o)).filter(Objects::nonNull).map(String::valueOf).collect(Collectors.toList()); 135 List<String> addedValues = newCollection.stream().filter(o -> !Utilities.caseInsensitiveContains(oldCollection, o)).filter(Objects::nonNull).map(String::valueOf).collect(Collectors.toList()); 136 137 if (!addedValues.isEmpty()) { 138 difference.setAddedValues(addedValues); 139 } 140 if (!removedValues.isEmpty()) { 141 difference.setRemovedValues(removedValues); 142 } 143 } 144 145 /** 146 * Adds a particular application name as case-insensitive 147 * @param application The application name 148 */ 149 public void addCaseInsensitiveApplication(String application) { 150 this.caseInsensitiveApplications.add(application); 151 } 152 153 /** 154 * Adds a particular field on a particular applicaiton as case-insensitive 155 * @param application The application name 156 * @param field The field name 157 */ 158 public void addCaseInsensitiveField(String application, String field) { 159 caseInsensitiveFields.add(application + ": " + field); 160 } 161 162 /** 163 * Adds a 'before' and 'after' to the rename map, which is used to find 164 * pairs of Links and also to figure out which Application's schema to use 165 * for difference detection. 166 * 167 * @param oldName The old name 168 * @param newName The new name 169 */ 170 public void addRenamedApplication(String oldName, String newName) { 171 this.renamedApplications.put(oldName, newName); 172 } 173 174 /** 175 * Handles the situation where the previous Link is null or has no attributes 176 * (usually, the current Link is brand new). All attributes are added as differences 177 * with only a 'new' value. 178 * 179 * The parameters to this method are all functional, allowing the various places 180 * this is used to customize the behavior as needed. 181 * 182 * @param differenceConsumer The callback to which each difference is passed 183 * @param isMultiValued A predicate that returns true if the given attribute (by name) is multi-valued 184 * @param getDisplayName A predicate that returns the display name of the given attribute (by name) 185 * @param afterAttributes The actual map of object attributes 186 */ 187 private void allNew(Consumer<Difference> differenceConsumer, Predicate<String> isMultiValued, Function<String, String> getDisplayName, Attributes<String, Object> afterAttributes) { 188 for (String key : afterAttributes.getKeys()) { 189 Object newValue = afterAttributes.get(key); 190 Difference difference = new Difference(); 191 difference.setAttribute(key); 192 difference.setNewValue(Utilities.safeString(newValue)); 193 difference.setMulti(isMultiValued.test(key)); 194 difference.setDisplayName(getDisplayName.apply(key)); 195 if (newValue instanceof Collection) { 196 addAddedRemovedValues(difference, null, newValue); 197 } 198 differenceConsumer.accept(difference); 199 } 200 } 201 202 /** 203 * Handles the situation where the previous Link is null or has no attributes 204 * (usually, the current Link has been deleted). All attributes are added as differences 205 * with only an 'old' value. 206 * 207 * The parameters to this method are all functional, allowing the various places 208 * this is used to customize the behavior as needed. 209 * 210 * @param differenceConsumer The callback to which each difference is passed 211 * @param isMultiValued A predicate that returns true if the given attribute (by name) is multi-valued 212 * @param getDisplayName A predicate that returns the display name of the given attribute (by name) 213 * @param beforeAttributes The actual map of object attributes 214 */ 215 private void allOld(Consumer<Difference> differenceConsumer, Predicate<String> isMultiValued, Function<String, String> getDisplayName, Attributes<String, Object> beforeAttributes) { 216 for (String key : beforeAttributes.getKeys()) { 217 Object oldValue = beforeAttributes.get(key); 218 Difference difference = new Difference(); 219 difference.setAttribute(key); 220 difference.setOldValue(Utilities.safeString(oldValue)); 221 difference.setMulti(isMultiValued.test(key)); 222 difference.setDisplayName(getDisplayName.apply(key)); 223 if (oldValue instanceof Collection) { 224 addAddedRemovedValues(difference, oldValue, null); 225 } 226 differenceConsumer.accept(difference); 227 } 228 } 229 230 /** 231 * Diffs the two snapshots and returns an IdentityDifference object containing all 232 * of the attribute, link, and role differences. 233 * 234 * TODO support permissions and policy violations 235 * 236 * @param before The before snapshot 237 * @param after The after snapshot 238 * @return Any differences between the two snapshots 239 * @throws GeneralException if any failures occur 240 */ 241 public IdentityDifference diff(IdentitySnapshot before, IdentitySnapshot after) throws GeneralException { 242 IdentityDifference differences = new IdentityDifference(); 243 diffIdentityAttributes(differences, before, after); 244 List<LinkPair> linkPairs = findLinkPairs(before, after); 245 for(LinkPair pair : linkPairs) { 246 String context; 247 if (pair.ls2 != null) { 248 context = IdentityDifference.generateContext(pair.ls2.getApplicationName(), pair.ls2.getNativeIdentity()); 249 } else { 250 context = IdentityDifference.generateContext(pair.ls1.getApplicationName(), pair.ls1.getNativeIdentity()); 251 } 252 diffLinks(differences, context, pair.ls1, pair.ls2); 253 } 254 255 // TODO I really need to figure out a good way to do role differences 256 // because there's so much more in the BundleSnapshot and the RoleAssignmentSnapshot 257 // that isn't easily captured by the Difference class. Notably, the associated 258 // entitlements and role targets. 259 List<String> rolesBefore = Utilities.safeStream(before.getBundles()).map(BundleSnapshot::getName).sorted().collect(Collectors.toList()); 260 List<String> rolesAfter = Utilities.safeStream(after.getBundles()).map(BundleSnapshot::getName).sorted().collect(Collectors.toList()); 261 Difference rolesDifference = Difference.diff(rolesBefore, rolesAfter, 4000, true); 262 if (rolesDifference != null) { 263 differences.addBundleDifference(rolesDifference); 264 } 265 266 List<String> assignmentsBefore = Utilities.safeStream(before.getAssignedRoles()).map(RoleAssignmentSnapshot::getName).sorted().collect(Collectors.toList()); 267 List<String> assignmentsAfter = Utilities.safeStream(after.getAssignedRoles()).map(RoleAssignmentSnapshot::getName).sorted().collect(Collectors.toList()); 268 Difference assignmentDifference = Difference.diff(assignmentsBefore, assignmentsAfter, 4000, true); 269 if (assignmentDifference != null) { 270 differences.addAssignedRoleDifference(assignmentDifference); 271 } 272 return differences; 273 } 274 275 /** 276 * Performs a diff of the two attribute maps provided, calling the supplied function hooks 277 * as needed to handle the details. 278 * 279 * @param beforeAttributes The attributes from "before" 280 * @param afterAttributes The attributes from "after" 281 * @param exclusions Any attributes to exclude from consideration 282 * @param isMultiValued A function that returns true if the given attribute is multi-valued 283 * @param getDisplayName A function that returns the display name of the given attribute 284 * @param differenceConsumer A handler to consume any Difference objects produced 285 */ 286 private void diff(String application, Attributes<String, Object> beforeAttributes, Attributes<String, Object> afterAttributes, List<String> exclusions, Predicate<String> isMultiValued, Function<String, String> getDisplayName, Consumer<Difference> differenceConsumer) { 287 Set<String> onlyBefore = new HashSet<>(); 288 Set<String> onlyAfter = new HashSet<>(); 289 Set<String> both = new HashSet<>(); 290 if (beforeAttributes == null) { 291 allNew(differenceConsumer, isMultiValued, getDisplayName, afterAttributes); 292 } else if (afterAttributes == null) { 293 allOld(differenceConsumer, isMultiValued, getDisplayName, beforeAttributes); 294 } else { 295 for (String key : beforeAttributes.keySet()) { 296 if (!afterAttributes.containsKey(key)) { 297 onlyBefore.add(key); 298 } else { 299 both.add(key); 300 } 301 } 302 for (String key : afterAttributes.keySet()) { 303 if (!beforeAttributes.containsKey(key)) { 304 onlyAfter.add(key); 305 } else { 306 both.add(key); 307 } 308 } 309 for(String exclusion : Util.safeIterable(exclusions)) { 310 onlyBefore.remove(exclusion); 311 onlyAfter.remove(exclusion); 312 both.remove(exclusion); 313 } 314 for (String key : both) { 315 Object oldValue = beforeAttributes.get(key); 316 Object newValue = afterAttributes.get(key); 317 if (!Differencer.objectsEqual(oldValue, newValue, true)) { 318 boolean ignoreCase = false; 319 if (caseInsensitiveApplications.contains(application)) { 320 ignoreCase = true; 321 } else { 322 String field = application + ": " + key; 323 if (caseInsensitiveFields.contains(field)) { 324 ignoreCase = true; 325 } 326 } 327 if (!Sameness.isSame(oldValue, newValue, ignoreCase)) { 328 Difference difference = new Difference(); 329 difference.setAttribute(key); 330 difference.setNewValue(Utilities.safeString(newValue)); 331 difference.setOldValue(Utilities.safeString(oldValue)); 332 difference.setMulti(isMultiValued.test(key)); 333 difference.setDisplayName(getDisplayName.apply(key)); 334 if (oldValue instanceof Collection || newValue instanceof Collection) { 335 addAddedRemovedValues(difference, oldValue, newValue); 336 } 337 differenceConsumer.accept(difference); 338 } 339 } 340 } 341 for (String key : onlyBefore) { 342 Object oldValue = beforeAttributes.get(key); 343 Difference difference = new Difference(); 344 difference.setAttribute(key); 345 difference.setOldValue(Utilities.safeString(oldValue)); 346 difference.setMulti(isMultiValued.test(key)); 347 difference.setDisplayName(getDisplayName.apply(key)); 348 if (oldValue instanceof Collection) { 349 addAddedRemovedValues(difference, oldValue, null); 350 } 351 differenceConsumer.accept(difference); 352 } 353 for (String key : onlyAfter) { 354 Object newValue = afterAttributes.get(key); 355 Difference difference = new Difference(); 356 difference.setAttribute(key); 357 difference.setNewValue(Utilities.safeString(newValue)); 358 difference.setMulti(isMultiValued.test(key)); 359 difference.setDisplayName(getDisplayName.apply(key)); 360 if (newValue instanceof Collection) { 361 addAddedRemovedValues(difference, null, newValue); 362 } 363 differenceConsumer.accept(difference); 364 } 365 } 366 } 367 368 private void diffIdentityAttributes(IdentityDifference difference, IdentitySnapshot before, IdentitySnapshot after) { 369 Attributes<String, Object> beforeAttributes = before.getAttributes(); 370 Attributes<String, Object> afterAttributes = after.getAttributes(); 371 final ObjectConfig identityAttributes = Identity.getObjectConfig(); 372 373 diff( 374 ProvisioningPlan.APP_IIQ, 375 beforeAttributes, 376 afterAttributes, 377 null, 378 a -> nullSafeObjectAttribute(identityAttributes, a).isMulti(), 379 a -> nullSafeObjectAttribute(identityAttributes, a).getDisplayName(), 380 difference::addAttributeDifference 381 ); 382 } 383 384 /** 385 * Detects the differences in the given LinkSnapshots and stores them in the 386 * IdentityDifference container 387 * 388 * @param differences The object into which differences ar added 389 * @param contextName The context name (the native ID) 390 * @param beforeLink The link before the change 391 * @param afterLink The link after the change 392 * @throws GeneralException if anything goes wrong 393 */ 394 private void diffLinks(IdentityDifference differences, String contextName, LinkSnapshot beforeLink, LinkSnapshot afterLink) throws GeneralException { 395 List<String> exclusions = Arrays.asList("directPermissions", "targetPermissions"); 396 Application application = null; 397 if (beforeLink != null) { 398 application = context.getObject(Application.class, beforeLink.getApplication()); 399 400 if (application == null) { 401 String renameMaybe = this.renamedApplications.get(beforeLink.getApplication()); 402 if (Util.isNotNullOrEmpty(renameMaybe)) { 403 application = context.getObject(Application.class, renameMaybe); 404 } 405 } 406 } 407 if (application == null && afterLink != null) { 408 application = context.getObject(Application.class, afterLink.getApplication()); 409 410 if (application == null) { 411 String renameMaybe = this.renamedApplications.get(afterLink.getApplication()); 412 if (Util.isNotNullOrEmpty(renameMaybe)) { 413 application = context.getObject(Application.class, renameMaybe); 414 } 415 } 416 } 417 if (application == null) { 418 // The application has probably been deleted. We can't do anything 419 // here because we need the application schema to continue. 420 // TODO Figure out if there's a way to fake the schema? 421 log.debug("Unable to find an application"); 422 if (beforeLink != null) { 423 log.debug("Before application is " + beforeLink.getApplication()); 424 } 425 if (afterLink != null) { 426 log.debug("After application is " + afterLink.getApplication()); 427 } 428 return; 429 } 430 boolean ignoreCase = false; 431 if (caseInsensitiveApplications.contains(application.getName()) || application.isCaseInsensitive()) { 432 ignoreCase = true; 433 } 434 Schema accountSchema = application.getAccountSchema(); 435 Attributes<String, Object> beforeAttributes = new Attributes<>(); 436 if (beforeLink != null && beforeLink.getAttributes() != null) { 437 beforeAttributes.putAll(beforeLink.getAttributes()); 438 } 439 Attributes<String, Object> afterAttributes = new Attributes<>(); 440 if (afterLink != null && afterLink.getAttributes() != null) { 441 afterAttributes.putAll(afterLink.getAttributes()); 442 } 443 List<Difference> linkDifferences = new ArrayList<>(); 444 diff( 445 application.getName(), 446 beforeAttributes, 447 afterAttributes, 448 exclusions, 449 a -> nullSafeObjectAttribute(accountSchema, a).isMultiValued(), 450 a -> nullSafeObjectAttribute(accountSchema, a).getDisplayName(), 451 d -> { 452 d.setContext(contextName); 453 linkDifferences.add(d); 454 } 455 ); 456 457 // Check the rename case 458 String beforeNativeIdentity = null; 459 String afterNativeIdentity = null; 460 if (beforeLink != null) { 461 beforeNativeIdentity = beforeLink.getNativeIdentity(); 462 } 463 if (afterLink != null) { 464 afterNativeIdentity = afterLink.getNativeIdentity(); 465 } 466 467 if (!Differencer.objectsEqual(beforeNativeIdentity, afterNativeIdentity, true)) { 468 Difference niDifference = new Difference(); 469 niDifference.setAttribute("nativeIdentity"); 470 niDifference.setContext(contextName); 471 niDifference.setOldValue(beforeNativeIdentity); 472 niDifference.setNewValue(afterNativeIdentity); 473 linkDifferences.add(niDifference); 474 } 475 476 differences.addLinkDifferences(linkDifferences); 477 478 List<Permission> beforePermissions = getPermissions(beforeLink); 479 List<Permission> afterPermissions = getPermissions(afterLink); 480 481 Collections.sort(beforePermissions, Comparator.comparing(Permission::getTarget).thenComparing(Permission::getRights)); 482 Collections.sort(afterPermissions, Comparator.comparing(Permission::getTarget).thenComparing(Permission::getRights)); 483 484 diffPermissions(beforeLink, afterLink, beforePermissions, afterPermissions, ignoreCase, differences::add); 485 } 486 487 /** 488 * Diffs the sorted permissions lists between the two link snapshots 489 * @param beforeLink The 'before' Link 490 * @param afterLink The 'after' Link 491 * @param beforePermissions The 'before' permissions, sorted 492 * @param afterPermissions The 'after' permissions, sorted 493 * @param ignoreCase If true, case will be ignored for comparison of target and rights 494 * @param differenceConsumer Differences will be passed to this callback for processing 495 */ 496 private void diffPermissions(LinkSnapshot beforeLink, LinkSnapshot afterLink, List<Permission> beforePermissions, List<Permission> afterPermissions, boolean ignoreCase, Consumer<PermissionDifference> differenceConsumer) { 497 List<Pair<Permission, Permission>> changes = new ArrayList<>(); 498 List<Permission> newPermissions = new ArrayList<>(); 499 if (afterPermissions != null) { 500 // Copy so we can remove them as we match 501 newPermissions.addAll(afterPermissions); 502 } 503 List<Permission> oldPermissions = new ArrayList<>(); 504 for(Permission p1 : Util.safeIterable(beforePermissions)) { 505 Permission p2 = findPermission(p1, newPermissions, ignoreCase); 506 if (p2 == null) { 507 oldPermissions.add(p1); 508 } else { 509 newPermissions.remove(p2); 510 Pair<Permission, Permission> pair = new Pair<>(p1, p2); 511 changes.add(pair); 512 } 513 } 514 for(Permission p1 : oldPermissions) { 515 PermissionDifference permissionDifference = new PermissionDifference(); 516 permissionDifference.setRights(p1.getRights()); 517 permissionDifference.setTarget(p1.getTarget()); 518 permissionDifference.setApplication(beforeLink.getApplicationName()); 519 permissionDifference.setRemoved(true); 520 differenceConsumer.accept(permissionDifference); 521 } 522 for(Pair<Permission, Permission> pair : changes) { 523 // Would be nice to be able to track before/after here, but PermissionDifference 524 // only tracks the after 525 Permission p2 = pair.getSecond(); 526 PermissionDifference permissionDifference = new PermissionDifference(); 527 permissionDifference.setRights(p2.getRights()); 528 permissionDifference.setTarget(p2.getTarget()); 529 permissionDifference.setApplication(afterLink.getApplicationName()); 530 differenceConsumer.accept(permissionDifference); 531 } 532 for(Permission p2 : newPermissions) { 533 PermissionDifference permissionDifference = new PermissionDifference(); 534 permissionDifference.setRights(p2.getRights()); 535 permissionDifference.setTarget(p2.getTarget()); 536 permissionDifference.setApplication(afterLink.getApplicationName()); 537 differenceConsumer.accept(permissionDifference); 538 } 539 } 540 541 /** 542 * Finds the a LinkSnapshot in the given list that matches the target by app, 543 * instance, and nativeIdentity 544 * 545 * @param snapshots The list of LinkSnapshots to search 546 * @param target The target to find 547 * @return the discovered LinkSnapshot, or null if none 548 */ 549 private LinkSnapshot findLink(List<LinkSnapshot> snapshots, LinkSnapshot target) { 550 for(LinkSnapshot link : Util.safeIterable(snapshots)) { 551 if (Differencer.objectsEqual(link.getApplicationName(), target.getApplicationName(), true) && Differencer.objectsEqual(link.getNativeIdentity(), target.getNativeIdentity(), true) && Differencer.objectsEqual(link.getInstance(), target.getInstance(), true)) { 552 return link; 553 } 554 } 555 556 String translatedName = this.renamedApplications.get(target.getApplicationName()); 557 if (Util.isNotNullOrEmpty(translatedName)) { 558 for(LinkSnapshot link : Util.safeIterable(snapshots)) { 559 if (Differencer.objectsEqual(link.getApplicationName(), translatedName, true) && Differencer.objectsEqual(link.getNativeIdentity(), target.getNativeIdentity(), true) && Differencer.objectsEqual(link.getInstance(), target.getInstance(), true)) { 560 return link; 561 } 562 } 563 } 564 565 return null; 566 } 567 568 /** 569 * Finds pairs of LinkSnapshots in the 'before' and 'after' snapshots by comparing 570 * them by identifier, native ID, or other matching methods. 571 * 572 * @param before The previous Identity Snapshot 573 * @param after The current Identity Snapshot 574 * @return The list of LinkPair objects 575 */ 576 private List<LinkPair> findLinkPairs(IdentitySnapshot before, IdentitySnapshot after) { 577 List<LinkPair> pairs = new ArrayList<>(); 578 List<LinkSnapshot> beforeLinks = safeCopy(before.getLinks()); 579 List<LinkSnapshot> afterLinks = safeCopy(after.getLinks()); 580 581 Iterator<LinkSnapshot> beforeIterator = beforeLinks.iterator(); 582 while(beforeIterator.hasNext()) { 583 LinkSnapshot ls1 = beforeIterator.next(); 584 LinkSnapshot ls2 = findLink(afterLinks, ls1); 585 if (ls2 != null) { 586 LinkPair pair = new LinkPair(ls1, ls2); 587 afterLinks.remove(ls2); 588 beforeIterator.remove(); 589 pairs.add(pair); 590 } 591 } 592 593 if (shouldGuessRenames() && !beforeLinks.isEmpty() && !afterLinks.isEmpty()) { 594 // Match by Application only for renames 595 Iterator<LinkSnapshot> beforeIterator2 = beforeLinks.iterator(); 596 while(beforeIterator2.hasNext()) { 597 LinkSnapshot ls1 = beforeIterator2.next(); 598 List<LinkSnapshot> candidates = findLinksBlindly(afterLinks, ls1); 599 if (candidates.size() == 1) { 600 // We found just one, assume it's a rename 601 LinkSnapshot ls2 = candidates.get(0); 602 if (looksLikeRename(ls1, ls2)) { 603 LinkPair pair = new LinkPair(ls1, ls2); 604 afterLinks.remove(ls2); 605 beforeIterator2.remove(); 606 pairs.add(pair); 607 } 608 } else if (candidates.size() > 1) { 609 // See if there are any that look like a rename (i.e. match by all other attributes except nativeIdentity) 610 for(LinkSnapshot candidate : candidates) { 611 if (looksLikeRename(ls1, candidate)) { 612 LinkSnapshot ls2 = candidates.get(0); 613 LinkPair pair = new LinkPair(ls1, ls2); 614 afterLinks.remove(ls2); 615 beforeIterator2.remove(); 616 pairs.add(pair); 617 break; 618 } 619 } 620 } 621 } 622 } 623 if (!afterLinks.isEmpty()) { 624 // New accounts 625 for(LinkSnapshot ls : afterLinks) { 626 LinkPair pair = new LinkPair(null, ls); 627 pairs.add(pair); 628 } 629 } 630 if (!beforeLinks.isEmpty()) { 631 // Deleted accounts 632 for(LinkSnapshot ls : beforeLinks) { 633 LinkPair pair = new LinkPair(ls, null); 634 pairs.add(pair); 635 } 636 } 637 638 return pairs; 639 } 640 641 /** 642 * Finds any LinkSnapshots in the given list that matches the target by app 643 * only, without checking the native identity. This is a last resort if the 644 * account has been renamed. 645 * 646 * @param snapshots The list of LinkSnapshots to search 647 * @param target The target to find 648 * @return the discovered LinkSnapshot, or null if none 649 */ 650 private List<LinkSnapshot> findLinksBlindly(List<LinkSnapshot> snapshots, LinkSnapshot target) { 651 List<LinkSnapshot> results = new ArrayList<>(); 652 for(LinkSnapshot link : Util.safeIterable(snapshots)) { 653 if (Differencer.objectsEqual(link.getApplicationName(), target.getApplicationName(), true)) { 654 results.add(link); 655 } 656 } 657 return results; 658 } 659 660 /** 661 * Finds the permission in the list matching p1. Permissions will be matched with decreasing specificity: 662 * 663 * 1) [Target, Rights, Annotation] 664 * 2) [Target, Rights] 665 * 3) [Target] 666 * 667 * Matching will be done using the {@link Sameness} class, meaning that list values will be compared independent of order (and possibly case). 668 * 669 * @param p1 The permission to match 670 * @param afterPermissions The list from which permissions will be matched 671 * @param ignoreCase If true, case will be ignored in comparison 672 * @return The matching Permission object from the list, or null 673 */ 674 private Permission findPermission(Permission p1, List<Permission> afterPermissions, boolean ignoreCase) { 675 Optional<Permission> permission = Utilities.safeStream(afterPermissions).filter(p2 -> Sameness.isSame(p2.getTarget(), p1.getTarget(), ignoreCase)).filter(p2 -> Sameness.isSame(p2.getRightsList(), p1.getRightsList(), ignoreCase)).filter(p2 -> Util.nullSafeEq(p1.getAnnotation(), p2.getAnnotation())).findFirst(); 676 if (permission.isPresent()) { 677 return permission.get(); 678 } 679 permission = Utilities.safeStream(afterPermissions).filter(p2 -> Sameness.isSame(p2.getTarget(), p1.getTarget(), ignoreCase)).filter(p2 -> Sameness.isSame(p2.getRightsList(), p1.getRightsList(), ignoreCase)).findFirst(); 680 if (permission.isPresent()) { 681 return permission.get(); 682 } 683 permission = Utilities.safeStream(afterPermissions).filter(p2 -> Sameness.isSame(p2.getTarget(), p1.getTarget(), ignoreCase)).findFirst(); 684 return permission.orElse(null); 685 } 686 687 /** 688 * Gets the Permission objects from the given LinkSnapshot. These are stored in two 689 * places: directPermissions and targetPermissions. 690 * 691 * @param link The link object 692 * @return The permissions, if any 693 */ 694 @SuppressWarnings("unchecked") 695 private List<Permission> getPermissions(LinkSnapshot link) { 696 List<Permission> permissions = new ArrayList<>(); 697 if (link == null || link.getAttributes() == null) { 698 return permissions; 699 } 700 Attributes<String, Object> attributes = link.getAttributes(); 701 if (attributes != null) { 702 List<Permission> perms = (List<Permission>)attributes.get("directPermissions"); 703 if (perms != null) { 704 permissions.addAll(perms); 705 } 706 707 perms = (List<Permission>)attributes.get("targetPermissions"); 708 if (perms != null) { 709 permissions.addAll(perms); 710 } 711 } 712 return permissions; 713 } 714 715 /** 716 * This looks like a rename if we differ only in nativeIdentity or if the Link IDs match 717 * @param ls1 The LinkSnapshot to check 718 * @param ls2 The other LinkSnapshot to check 719 * @return True if this looks like a rename 720 */ 721 private boolean looksLikeRename(LinkSnapshot ls1, LinkSnapshot ls2) { 722 if (Differencer.objectsEqual(ls1.getId(), ls2.getId(), false)) { 723 return true; 724 } 725 if (!Differencer.objectsEqual(ls1.getNativeIdentity(), ls2.getNativeIdentity(), true)) { 726 return Difference.equal(ls1.getAttributes(), ls2.getAttributes()); 727 } 728 return false; 729 } 730 731 private AttributeDefinition nullSafeObjectAttribute(Schema source, String name) { 732 AttributeDefinition attr = source.getAttributeDefinition(name); 733 if (attr == null) { 734 attr = new AttributeDefinition(); 735 } 736 return attr; 737 } 738 739 private ObjectAttribute nullSafeObjectAttribute(ObjectConfig source, String name) { 740 ObjectAttribute attr = source.getObjectAttribute(name); 741 if (attr == null) { 742 attr = new ObjectAttribute(); 743 } 744 return attr; 745 } 746 747 /** 748 * Creates a shallow copy of a possibly null list. If the list is null, an 749 * empty list will be returned. 750 * 751 * @param source The source list to copy 752 * @param <T> The type tag of the list 753 * @return A shallow copy of the list, or an empty list if the original is null 754 */ 755 private <T> List<T> safeCopy(List<T> source) { 756 List<T> copy = new ArrayList<>(); 757 if (source != null) { 758 copy.addAll(source); 759 } 760 return copy; 761 } 762 763 /** 764 * Sets the 'guess renames' flag to true. If true, the BetterDifferencer will attempt 765 * to guess which Link corresponds to the one in the previous snapshot. 766 * 767 * @param guessRenames The flag to set 768 */ 769 public void setGuessRenames(boolean guessRenames) { 770 this.guessRenames = guessRenames; 771 } 772 773 /** 774 * @return The value of the 'guess renames' flag 775 */ 776 public boolean shouldGuessRenames() { 777 return guessRenames; 778 } 779}