001package com.identityworksllc.iiq.common; 002 003import com.fasterxml.jackson.databind.ObjectMapper; 004import com.identityworksllc.iiq.common.annotation.CoreStable; 005import com.identityworksllc.iiq.common.annotation.Experimental; 006import com.identityworksllc.iiq.common.vo.OutcomeType; 007import sailpoint.api.Aggregator; 008import sailpoint.api.Identitizer; 009import sailpoint.api.RequestManager; 010import sailpoint.api.SailPointContext; 011import sailpoint.connector.Connector; 012import sailpoint.connector.ConnectorException; 013import sailpoint.connector.ConnectorFactory; 014import sailpoint.object.*; 015import sailpoint.object.ProvisioningPlan.AccountRequest; 016import sailpoint.object.ProvisioningPlan.AttributeRequest; 017import sailpoint.request.AggregateRequestExecutor; 018import sailpoint.service.ServiceHandler; 019import sailpoint.tools.*; 020import sailpoint.tools.xml.XMLReferenceResolver; 021 022import java.util.*; 023 024/** 025 * This class contains several utilities for dealing with accounts and applications. 026 * In particular, the {@link #aggregateAccount(AggregateOptions)} method is provided to 027 * simplify the process of aggregating a single account (either pulling it from the 028 * connector by name, or constructing an instance yourself) into IIQ. There are several 029 * overloaded versions of this method for convenience. 030 * 031 * As a core utility, this class is guaranteed to remain stable, except after a lengthy 032 * deprecation period. New functionality may be added, but existing functionality will 033 * not be removed or changed in a breaking way. 034 */ 035@CoreStable 036@SuppressWarnings("unused") 037public class AccountUtilities extends AbstractBaseUtility { 038 039 /** 040 * The input options to {@link #aggregateAccounts(MultipleAggregateOptions)}. 041 */ 042 public static class MultipleAggregateOptions { 043 /** 044 * The account filter, compiled (may be null) 045 */ 046 private Filter accountFilter; 047 048 /** 049 * The account filter string, pre-compilation 050 */ 051 private String accountFilterString; 052 053 /** 054 * The accounts list to aggregate; the input pairs are (application name, native identity) 055 */ 056 private List<Pair<String, String>> accountsList; 057 058 /** 059 * The options to pass to the Aggregator 060 */ 061 private Map<String, Object> aggregateOptions; 062 063 /** 064 * The identity from whom to pull the list of accounts to aggregate 065 */ 066 private String identity; 067 068 /** 069 * No-args constructor allowing all options to be configured using setters 070 */ 071 public MultipleAggregateOptions() { 072 this.accountsList = new ArrayList<>(); 073 this.aggregateOptions = new HashMap<>(); 074 } 075 076 /** 077 * Gets the account filter, if any 078 * @return The account filter, or null if none was set 079 */ 080 public Filter getAccountFilter() { 081 return accountFilter; 082 } 083 084 /** 085 * Gets the account filter string, if any 086 * @return The account filter string, or null if none was set 087 */ 088 public String getAccountFilterString() { 089 return accountFilterString; 090 } 091 092 /** 093 * Gets the list of accounts to aggregate 094 * @return The accounts list, or null if none was set 095 */ 096 public List<Pair<String, String>> getAccountsList() { 097 return accountsList; 098 } 099 100 /** 101 * Gets the options to pass to the Aggregator 102 * @return The aggregate options, or null if none was set 103 */ 104 public Map<String, Object> getAggregateOptions() { 105 return aggregateOptions; 106 } 107 108 /** 109 * Gets the identity from whom to pull accounts 110 * @return The identity name, or null if none was set 111 */ 112 public String getIdentity() { 113 return identity; 114 } 115 116 /** 117 * Sets the account filter 118 * @param accountFilter The account filter 119 */ 120 public void setAccountFilter(Filter accountFilter) { 121 this.accountFilter = accountFilter; 122 } 123 124 /** 125 * Sets the account filter string 126 * @param accountFilterString The account filter string 127 */ 128 public void setAccountFilterString(String accountFilterString) { 129 this.accountFilterString = accountFilterString; 130 } 131 132 /** 133 * Sets the list of accounts to aggregate. The input pairs are (application name, native identity). 134 * @param accountsList The accounts list 135 */ 136 public void setAccountsList(List<Pair<String, String>> accountsList) { 137 this.accountsList = accountsList; 138 } 139 140 /** 141 * Sets the options to pass to the Aggregator 142 * @param aggregateOptions The aggregate options 143 */ 144 public void setAggregateOptions(Map<String, Object> aggregateOptions) { 145 this.aggregateOptions = aggregateOptions; 146 } 147 148 /** 149 * Sets the identity from whom to pull accounts 150 * @param identity The identity name 151 */ 152 public void setIdentity(String identity) { 153 this.identity = identity; 154 } 155 156 } 157 158 /** 159 * The options class for {@link #aggregateAccount(AggregateOptions)}. This class may be 160 * modified without warning by adding new constructors, getters, and setters, but existing 161 * functionality will not be removed or changed in a breaking way. 162 */ 163 public static class AggregateOptions { 164 165 /** 166 * The name of the account to pull via getObject() 167 */ 168 private String accountName; 169 170 /** 171 * The options to pass to the Aggregator 172 */ 173 private Map<String, Object> aggregateOptions; 174 175 /** 176 * The Application object, which should be attached to the current context 177 */ 178 private Application application; 179 180 /** 181 * The application name 182 */ 183 private String applicationName; 184 185 /** 186 * The Connector object 187 */ 188 private Connector connector; 189 190 /** 191 * True if we should force aggregation even for connectors that don't explicitly support getObject() 192 */ 193 private boolean forceAggregate; 194 195 /** 196 * True if we should consider the input to be incomplete 197 */ 198 private boolean incomplete; 199 200 /** 201 * True if we should attempt to refresh the Identity afterwards 202 */ 203 private boolean refreshIdentity; 204 205 /** 206 * Options to pass to the Identitizer 207 */ 208 private Map<String, Object> refreshOptions; 209 210 /** 211 * The ResourceObject that will be aggregated 212 */ 213 private ResourceObject resourceObject; 214 215 /** 216 * True if we should run the customization rules. You should set this if your 217 * aggregation is "manual" (i.e., you are providing the Map or ResourceObject, rather 218 * than reading it from the connector). 219 */ 220 private boolean runAppCustomization; 221 222 /** 223 * No-args constructor allowing all options to be configured using setters 224 */ 225 public AggregateOptions() { 226 this.aggregateOptions = new HashMap<>(); 227 this.refreshOptions = new HashMap<>(); 228 } 229 230 /** 231 * Constructs an AggregateOptions for importing the given Map as though it was returned 232 * from the given application. The Map will be cloned before passing it to ResourceObject. 233 * 234 * @param applicationName The name of the Application 235 * @param inputs The input data 236 */ 237 public AggregateOptions(String applicationName, Map<String, Object> inputs) { 238 this(); 239 this.applicationName = applicationName; 240 241 ResourceObject resourceObject = new ResourceObject(); 242 resourceObject.setAttributes(new Attributes<>(inputs)); 243 244 this.resourceObject = resourceObject; 245 this.runAppCustomization = true; 246 } 247 248 /** 249 * Constructs an AggregateOptions for importing the given Map as though it was returned 250 * from the given application. 251 * 252 * @param application The application 253 * @param inputs The input data 254 * @throws GeneralException if the connector cannot be loaded 255 */ 256 public AggregateOptions(Application application, Map<String, Object> inputs) throws GeneralException { 257 this(); 258 this.application = application; 259 this.applicationName = application.getName(); 260 this.connector = ConnectorFactory.getConnector(application, null); 261 262 ResourceObject resourceObject = new ResourceObject(); 263 resourceObject.setAttributes(new Attributes<>(inputs)); 264 265 this.resourceObject = resourceObject; 266 this.runAppCustomization = true; 267 } 268 269 /** 270 * Constructs an AggregateOptions for importing the given Map as though it was returned 271 * from the given application. 272 * 273 * @param application The application 274 * @param inputs The input data 275 * @throws GeneralException if the connector cannot be loaded 276 */ 277 public AggregateOptions(Application application, ResourceObject inputs) throws GeneralException { 278 this(); 279 this.application = application; 280 this.applicationName = application.getName(); 281 this.connector = ConnectorFactory.getConnector(application, null); 282 this.resourceObject = inputs; 283 } 284 285 private AggregateOptions(AggregateOptionsBuilder builder) { 286 setAccountName(builder.accountName); 287 setAggregateOptions(builder.aggregateOptions); 288 setApplication(builder.application); 289 setApplicationName(builder.applicationName); 290 setConnector(builder.connector); 291 setForceAggregate(builder.forceAggregate); 292 setIncomplete(builder.incomplete); 293 setRefreshIdentity(builder.refreshIdentity); 294 setRefreshOptions(builder.refreshOptions); 295 setResourceObject(builder.resourceObject); 296 setRunAppCustomization(builder.runAppCustomization); 297 } 298 299 /** 300 * Creates a new fluent builder to build {@link AggregateOptions}. 301 * @return the {@link AggregateOptionsBuilder} 302 */ 303 public static AggregateOptionsBuilder builder() { 304 return new AggregateOptionsBuilder(); 305 } 306 307 /** 308 * Creates a new {@link AggregateOptionsBuilder} initialized with the values of the provided 309 * {@code AggregateOptions} instance. 310 * 311 * @param copy the instance to copy values from 312 * @return the {@link AggregateOptionsBuilder} 313 */ 314 public static AggregateOptionsBuilder builder(AggregateOptions copy) { 315 AggregateOptionsBuilder builder = new AggregateOptionsBuilder(); 316 builder.accountName = copy.getAccountName(); 317 builder.aggregateOptions = copy.getAggregateOptions(); 318 builder.application = copy.getApplication(); 319 builder.applicationName = copy.getApplicationName(); 320 builder.connector = copy.getConnector(); 321 builder.forceAggregate = copy.isForceAggregate(); 322 builder.incomplete = copy.isIncomplete(); 323 builder.refreshIdentity = copy.isRefreshIdentity(); 324 builder.refreshOptions = copy.getRefreshOptions(); 325 builder.resourceObject = copy.getResourceObject(); 326 builder.runAppCustomization = copy.isRunAppCustomization(); 327 return builder; 328 } 329 330 /** 331 * Adds a new aggregate option to the existing Map, creating the Map if it is 332 * null. 333 * 334 * @param option The option 335 * @param value The value 336 */ 337 public void addAggregateOption(String option, Object value) { 338 if (this.aggregateOptions == null) { 339 this.aggregateOptions = new HashMap<>(); 340 } 341 342 this.aggregateOptions.put(option, value); 343 } 344 345 public String getAccountName() { 346 return accountName; 347 } 348 349 public Map<String, Object> getAggregateOptions() { 350 return aggregateOptions; 351 } 352 353 public Application getApplication() { 354 return application; 355 } 356 357 /** 358 * Gets the application object if it is manually set. Otherwise, loads it using the 359 * application name. 360 * 361 * @param context The context to use to load the application, if needed 362 * @return The Application object, or null if the application name is not set in the options and it does not exist in IIQ 363 * @throws GeneralException if the application does not exist 364 */ 365 public Application getApplication(SailPointContext context) throws GeneralException { 366 if (this.application != null) { 367 return this.application; 368 } else { 369 return context.getObject(Application.class, this.applicationName); 370 } 371 } 372 373 public String getApplicationName() { 374 return applicationName; 375 } 376 377 public Connector getConnector() { 378 return connector; 379 } 380 381 public Map<String, Object> getRefreshOptions() { 382 return refreshOptions; 383 } 384 385 public ResourceObject getResourceObject() { 386 return resourceObject; 387 } 388 389 public boolean isForceAggregate() { 390 return forceAggregate; 391 } 392 393 public boolean isIncomplete() { 394 return incomplete; 395 } 396 397 public boolean isRefreshIdentity() { 398 return refreshIdentity; 399 } 400 401 public boolean isRunAppCustomization() { 402 return runAppCustomization; 403 } 404 405 public void setAccountName(String accountName) { 406 this.accountName = accountName; 407 } 408 409 public void setAggregateOptions(Map<String, Object> aggregateOptions) { 410 this.aggregateOptions = aggregateOptions; 411 } 412 413 public void setApplication(Application application) { 414 this.application = application; 415 } 416 417 public void setApplicationName(String applicationName) { 418 this.applicationName = applicationName; 419 } 420 421 public void setConnector(Connector connector) { 422 this.connector = connector; 423 } 424 425 public void setCorrelateOnly(boolean flag) { 426 this.addAggregateOption(Aggregator.ARG_CORRELATE_ONLY, flag); 427 } 428 429 public void setForceAggregate(boolean forceAggregate) { 430 this.forceAggregate = forceAggregate; 431 } 432 433 public void setIncomplete(boolean incomplete) { 434 this.incomplete = incomplete; 435 } 436 437 public void setRefreshIdentity(boolean refreshIdentity) { 438 this.refreshIdentity = refreshIdentity; 439 } 440 441 public void setRefreshOptions(Map<String, Object> refreshOptions) { 442 this.refreshOptions = refreshOptions; 443 } 444 445 public void setResourceObject(ResourceObject resourceObject) { 446 this.resourceObject = resourceObject; 447 } 448 449 public void setRunAppCustomization(boolean runAppCustomization) { 450 this.runAppCustomization = runAppCustomization; 451 } 452 453 public void setTrace(boolean flag) { 454 this.addAggregateOption(Aggregator.ARG_TRACE, flag); 455 } 456 } 457 458 /** 459 * {@code AggregateOptions} builder static inner class. 460 */ 461 @Experimental 462 public static final class AggregateOptionsBuilder { 463 private String accountName; 464 private Map<String, Object> aggregateOptions; 465 private Application application; 466 private String applicationName; 467 private Connector connector; 468 private boolean forceAggregate; 469 private boolean incomplete; 470 private boolean refreshIdentity; 471 private Map<String, Object> refreshOptions; 472 private ResourceObject resourceObject; 473 private boolean runAppCustomization; 474 475 private AggregateOptionsBuilder() { 476 } 477 478 public static AggregateOptionsBuilder builder() { 479 return new AggregateOptionsBuilder(); 480 } 481 482 /** 483 * Returns a {@code AggregateOptions} built from the parameters previously set. 484 * 485 * @return a {@code AggregateOptions} built with parameters of this {@code AggregateOptions.Builder} 486 */ 487 public AggregateOptions build() { 488 return new AggregateOptions(this); 489 } 490 491 /** 492 * Sets the {@code accountName} and returns a reference to this Builder enabling method chaining. 493 * 494 * @param val the {@code accountName} to set 495 * @return a reference to this Builder 496 */ 497 public AggregateOptionsBuilder withAccountName(String val) { 498 accountName = val; 499 return this; 500 } 501 502 /** 503 * Sets the {@code aggregateOptions} and returns a reference to this Builder enabling method chaining. 504 * 505 * @param val the {@code aggregateOptions} to set 506 * @return a reference to this Builder 507 */ 508 public AggregateOptionsBuilder withAggregateOptions(Map<String, Object> val) { 509 aggregateOptions = val; 510 return this; 511 } 512 513 /** 514 * Sets the {@code application} and returns a reference to this Builder enabling method chaining. 515 * 516 * @param val the {@code application} to set 517 * @return a reference to this Builder 518 */ 519 public AggregateOptionsBuilder withApplication(Application val) { 520 application = val; 521 return this; 522 } 523 524 /** 525 * Sets the {@code applicationName} and returns a reference to this Builder enabling method chaining. 526 * 527 * @param val the {@code applicationName} to set 528 * @return a reference to this Builder 529 */ 530 public AggregateOptionsBuilder withApplicationName(String val) { 531 applicationName = val; 532 return this; 533 } 534 535 /** 536 * Sets the {@code connector} and returns a reference to this Builder enabling method chaining. 537 * 538 * @param val the {@code connector} to set 539 * @return a reference to this Builder 540 */ 541 public AggregateOptionsBuilder withConnector(Connector val) { 542 connector = val; 543 return this; 544 } 545 546 /** 547 * Sets the {@code forceAggregate} and returns a reference to this Builder enabling method chaining. 548 * 549 * @param val the {@code forceAggregate} to set 550 * @return a reference to this Builder 551 */ 552 public AggregateOptionsBuilder withForceAggregate(boolean val) { 553 forceAggregate = val; 554 return this; 555 } 556 557 /** 558 * Sets the {@code incomplete} and returns a reference to this Builder enabling method chaining. 559 * 560 * @param val the {@code incomplete} to set 561 * @return a reference to this Builder 562 */ 563 public AggregateOptionsBuilder withIncomplete(boolean val) { 564 incomplete = val; 565 return this; 566 } 567 568 /** 569 * Sets the {@code refreshIdentity} and returns a reference to this Builder enabling method chaining. 570 * 571 * @param val the {@code refreshIdentity} to set 572 * @return a reference to this Builder 573 */ 574 public AggregateOptionsBuilder withRefreshIdentity(boolean val) { 575 refreshIdentity = val; 576 return this; 577 } 578 579 /** 580 * Sets the {@code refreshOptions} and returns a reference to this Builder enabling method chaining. 581 * 582 * @param val the {@code refreshOptions} to set 583 * @return a reference to this Builder 584 */ 585 public AggregateOptionsBuilder withRefreshOptions(Map<String, Object> val) { 586 refreshOptions = val; 587 return this; 588 } 589 590 /** 591 * Sets the {@code resourceObject} and returns a reference to this Builder enabling method chaining. 592 * 593 * @param val the {@code resourceObject} to set 594 * @return a reference to this Builder 595 */ 596 public AggregateOptionsBuilder withResourceObject(ResourceObject val) { 597 resourceObject = val; 598 return this; 599 } 600 601 /** 602 * Sets the {@code runAppCustomization} and returns a reference to this Builder enabling method chaining. 603 * 604 * @param val the {@code runAppCustomization} to set 605 * @return a reference to this Builder 606 */ 607 public AggregateOptionsBuilder withRunAppCustomization(boolean val) { 608 runAppCustomization = val; 609 return this; 610 } 611 } 612 613 /** 614 * The list of tokens that likely indicate a password type variable 615 */ 616 private static final List<String> likelyPasswordTokens = Arrays.asList("password", "unicodepwd", "secret", "private"); 617 /** 618 * The internal provisioning utilities object 619 */ 620 private final ProvisioningUtilities provisioningUtilities; 621 /** 622 * Constructor 623 * 624 * @param c The current SailPointContext 625 */ 626 public AccountUtilities(SailPointContext c) { 627 this(c, new ProvisioningUtilities(c)); 628 } 629 630 /** 631 * Constructor allowing you to pass a new ProvisioningUtilities 632 * 633 * @param c The context 634 * @param provisioningUtilities A pre-existing provisioning utilities 635 */ 636 public AccountUtilities(SailPointContext c, ProvisioningUtilities provisioningUtilities) { 637 super(c); 638 639 this.provisioningUtilities = Objects.requireNonNull(provisioningUtilities); 640 } 641 642 /** 643 * Fixes the Identity and display name of the given Resource Object by extracting it 644 * from the ResourceObject based on the identity field defined in the application. 645 * Connectors typically do this, but a manually constructed ResourceObject probably 646 * will not. 647 * 648 * @param resourceObject The ResourceObject input to modify 649 * @param application The Application that the ResourceObject belongs to 650 */ 651 public static void fixResourceObjectIdentity(ResourceObject resourceObject, Application application) { 652 if (Util.isNullOrEmpty(resourceObject.getIdentity())) { 653 String identityField = application.getAccountSchema().getIdentityAttribute(); 654 if (Util.isNotNullOrEmpty(identityField)) { 655 String identityValue = resourceObject.getStringAttribute(identityField); 656 if (Util.isNotNullOrEmpty(identityValue)) { 657 resourceObject.setIdentity(identityValue); 658 } 659 } 660 661 String displayField = application.getAccountSchema().getDisplayAttribute(); 662 if (Util.isNotNullOrEmpty(displayField)) { 663 String displayValue = resourceObject.getStringAttribute(displayField); 664 if (Util.isNotNullOrEmpty(displayValue)) { 665 resourceObject.setDisplayName(displayValue); 666 } 667 } 668 } 669 } 670 671 /** 672 * Aggregates the account, given the options as a Map. The Map will be decoded 673 * into an {@link AggregateOptions} object. 674 * 675 * The return value will also be a Map. 676 * 677 * This simplified interface is intended for situations where this class is only 678 * available via reflection, such as a third-party plugin. 679 * 680 * @param optionsMap The options map 681 * @return The {@link AggregationOutcome}, serialized to a Map via Jackson 682 * @throws GeneralException on any errors 683 */ 684 @SuppressWarnings({"unchecked", "rawtypes"}) 685 public Map<String, Object> aggregateAccount(Map<String, Object> optionsMap) throws GeneralException { 686 AggregateOptions options = new AggregateOptions(); 687 688 if (optionsMap.get("applicationName") instanceof String) { 689 options.setApplicationName((String) optionsMap.get("applicationName")); 690 } else if (optionsMap.get("application") instanceof Application) { 691 options.setApplication((Application) optionsMap.get("application")); 692 } else { 693 throw new GeneralException("Your input map must include either 'applicationName' or 'application'"); 694 } 695 696 boolean defaultRunCustomization = false; 697 boolean needsNativeIdentity = true; 698 if (optionsMap.get("resourceObject") != null) { 699 Object ro = optionsMap.get("resourceObject"); 700 if (ro instanceof Map) { 701 ResourceObject resourceObject = new ResourceObject(); 702 resourceObject.setAttributes(new Attributes((Map)ro)); 703 options.setResourceObject(resourceObject); 704 needsNativeIdentity = false; 705 defaultRunCustomization = true; 706 } else if (ro instanceof ResourceObject) { 707 options.setResourceObject((ResourceObject) ro); 708 needsNativeIdentity = false; 709 defaultRunCustomization = true; 710 } else { 711 throw new GeneralException("The input map has a 'resourceObject', but is neither a Map nor a ResourceObject"); 712 } 713 } 714 715 if (optionsMap.get("nativeIdentity") != null) { 716 options.setAccountName((String) optionsMap.get("nativeIdentity")); 717 } else if (optionsMap.get("accountName") != null) { 718 options.setAccountName((String) optionsMap.get("accountName")); 719 } else if (needsNativeIdentity) { 720 throw new GeneralException("Your input map must include a 'resourceObject' and/or 'nativeIdentity' or 'accountName'"); 721 } 722 723 if (optionsMap.containsKey("runAppCustomization")) { 724 options.setRunAppCustomization(Utilities.isFlagSet(optionsMap.get("runAppCustomization"))); 725 } else { 726 options.setRunAppCustomization(defaultRunCustomization); 727 } 728 729 options.setRunAppCustomization(Utilities.isFlagSet(optionsMap.get("incomplete"))); 730 731 if (optionsMap.get("aggregateOptions") instanceof Map) { 732 options.setAggregateOptions((Map) optionsMap.get("aggregateOptions")); 733 } 734 735 AggregationOutcome outcome = aggregateAccount(options); 736 737 try { 738 ObjectMapper jacksonMapper = new ObjectMapper(); 739 740 return jacksonMapper.convertValue(outcome, Map.class); 741 } catch(Exception e) { 742 throw new GeneralException("Aggregation finished, but serializing the output to Map failed", e); 743 } 744 } 745 746 /** 747 * Executes an aggregation according to the given options. This may be invoked directly or 748 * via one of the many overloaded shortcut methods. 749 * 750 * @param options The aggregation options 751 * @return An AggregationOutcome object, with various 752 * @throws GeneralException if any aggregation failures occur 753 */ 754 public AggregationOutcome aggregateAccount(AggregateOptions options) throws GeneralException { 755 final Set<String> allowForce = new HashSet<>(Collections.singletonList("DelimitedFile")); 756 757 Application appObject = options.getApplication(context); 758 759 if (options.application == null) { 760 options.application = appObject; 761 } 762 763 if (options.connector == null) { 764 options.connector = ConnectorFactory.getConnector(appObject, null); 765 } 766 767 AggregationOutcome outcome = new AggregationOutcome(options.getApplicationName(), options.getAccountName()); 768 outcome.setStartTimeMillis(System.currentTimeMillis()); 769 770 // This will only be the case if we have not actually done the getObject() yet 771 if (options.resourceObject == null) { 772 try { 773 for (Application.Feature feature : options.connector.getApplication().getFeatures()) { 774 if (feature.equals(Application.Feature.NO_RANDOM_ACCESS)) { 775 if (!(options.forceAggregate && allowForce.contains(appObject.getType()))) { 776 outcome.setStatus(OutcomeType.Skipped); 777 outcome.addMessage("Application " + appObject.getName() + " does not support random access"); 778 return outcome; 779 } 780 } 781 } 782 if (appObject.getType().equals("ServiceNow")) { 783 options.resourceObject = doServiceNowConnectorHack("sys_id", options.accountName, appObject, false); 784 } else { 785 options.resourceObject = options.connector.getObject("account", options.accountName, null); 786 } 787 if (options.resourceObject == null) { 788 outcome.setStatus(OutcomeType.Skipped); 789 outcome.addMessage(Message.warn("getObject() returned null")); 790 log.warn("getObject() for application = '" + options.application.getName() + "', native identity = '" + options.accountName + "' returned null"); 791 return outcome; 792 } 793 } catch (ConnectorException onfe) { 794 throw new GeneralException(onfe); 795 } 796 } 797 798 fixResourceObjectIdentity(options.resourceObject, options.application); 799 800 // Normally, this is done as part of getObject() by the connector, so we only want to run it 801 // in the case of a truly manual aggregation, i.e., constructing a fake ResourceObject to pass in 802 if (options.runAppCustomization) { 803 Rule customizationRule = appObject.getCustomizationRule(); 804 805 ResourceObject customizationOutput = options.resourceObject; 806 807 if (customizationRule != null) { 808 customizationOutput = runCustomizationRule(customizationRule, options, outcome); 809 } 810 811 // Abort if customization failed 812 if (outcome.getStatus() == OutcomeType.Failure || outcome.getStatus() == OutcomeType.Warning) { 813 log.warn("Application customization rule failed"); 814 return outcome; 815 } 816 817 if (customizationOutput == null) { 818 outcome.setStatus(OutcomeType.Skipped); 819 outcome.addMessage(Message.warn("Application customization rule returned null")); 820 log.warn("Application customization rule for application = '" + options.application.getName() + "', native identity = '" + options.accountName + "' returned null"); 821 return outcome; 822 } 823 824 options.resourceObject = customizationOutput; 825 826 fixResourceObjectIdentity(options.resourceObject, options.application); 827 828 if (appObject.getAccountSchema() != null && appObject.getAccountSchema().getCustomizationRule() != null) { 829 customizationOutput = runCustomizationRule(appObject.getAccountSchema().getCustomizationRule(), options, outcome); 830 } 831 832 // Abort if customization failed 833 if (outcome.getStatus() == OutcomeType.Failure || outcome.getStatus() == OutcomeType.Warning) { 834 log.warn("Application customization rule failed"); 835 return outcome; 836 } 837 838 if (customizationOutput == null) { 839 outcome.setStatus(OutcomeType.Skipped); 840 outcome.addMessage(Message.warn("Schema customization rule returned null")); 841 log.warn("Schema customization rule for application = '" + options.application.getName() + "', native identity = '" + options.accountName + "' returned null"); 842 return outcome; 843 } 844 845 options.resourceObject = customizationOutput; 846 847 fixResourceObjectIdentity(options.resourceObject, options.application); 848 } 849 850 if (options.incomplete) { 851 options.resourceObject.setIncomplete(true); 852 } 853 854 Attributes<String, Object> argMap = new Attributes<>(); 855 argMap.put(Identitizer.ARG_PROMOTE_ATTRIBUTES, true); 856 argMap.put(Aggregator.ARG_NO_OPTIMIZE_REAGGREGATION, true); 857 argMap.put(Identitizer.ARG_ALWAYS_REFRESH_MANAGER, true); 858 859 if (options.aggregateOptions != null) { 860 argMap.putAll(options.aggregateOptions); 861 } 862 863 Aggregator agg = new Aggregator(context, argMap); 864 agg.setMaxIdentities(1); 865 TaskResult taskResult = agg.aggregate(appObject, options.resourceObject); 866 867 if (null == taskResult) { 868 throw new IllegalStateException("Aggregator.aggregate() returned null unexpectedly"); 869 } 870 871 outcome.setTaskResult(taskResult); 872 outcome.setNativeIdentity(options.resourceObject.getIdentity()); 873 outcome.setStatus(OutcomeType.Success); 874 875 if (options.refreshIdentity) { 876 QueryOptions qo = new QueryOptions(); 877 qo.addFilter(Filter.eq("application.name", options.application.getName())); 878 qo.addFilter(Filter.eq("nativeIdentity", options.resourceObject.getIdentity())); 879 880 List<Link> linkCandidates = context.getObjects(Link.class, qo); 881 882 if (linkCandidates.size() > 1) { 883 String warning = "Aggregation produced more than one Link with the same Native Identity: " + options.resourceObject.getIdentity(); 884 log.warn(warning); 885 outcome.addMessage(Message.warn(warning)); 886 } else if (linkCandidates.size() == 1) { 887 Link theLink = linkCandidates.get(0); 888 Identity identity = theLink.getIdentity(); 889 if (identity != null) { 890 BaseIdentityUtilities identityUtilities = new BaseIdentityUtilities(context); 891 Attributes<String, Object> refreshOptions = identityUtilities.getDefaultRefreshOptions(false); 892 if (!Util.isEmpty(options.refreshOptions)) { 893 refreshOptions.putAll(options.refreshOptions); 894 } 895 identityUtilities.refresh(identity, refreshOptions); 896 897 outcome.setIdentityName(identity.getName()); 898 outcome.setRefreshed(true); 899 } 900 } else { 901 String warning = "After aggregation, no Link found with application = " + appObject.getName() + ", native identity = " + options.resourceObject.getIdentity(); 902 log.warn(warning); 903 outcome.addMessage(Message.warn(warning)); 904 } 905 } 906 907 outcome.setStopTimeMillis(System.currentTimeMillis()); 908 909 return outcome; 910 } 911 912 /** 913 * Aggregates the given {@link ResourceObject} into IIQ as though it was pulled in via an aggregation task. 914 * 915 * Identical to {@link #aggregateAccount(Application, Connector, ResourceObject, boolean, Map)} 916 * with an empty Map for the final parameter. 917 * 918 * @param appObject The application objet 919 * @param appConnector The connector object 920 * @param rObj The ResourceObject, either pulled from the Connector yourself or constructed manually 921 * @param refreshIdentity If true, refresh the Identity after aggregation 922 * @return The aggregation outcomes 923 * @throws GeneralException if any IIQ failure occurs 924 */ 925 public AggregationOutcome aggregateAccount(Application appObject, Connector appConnector, ResourceObject rObj, boolean refreshIdentity) throws GeneralException { 926 return this.aggregateAccount(appObject, appConnector, rObj, refreshIdentity, new HashMap<>()); 927 } 928 929 /** 930 * Aggregates the given {@link ResourceObject} into IIQ as though it was pulled in via an aggregation task 931 * @param appObject The application objet 932 * @param appConnector The connector object 933 * @param resource The ResourceObject, either pulled from the Connector or constructed 934 * @param refreshIdentity If true, refresh the Identity after aggregation 935 * @param aggregateArguments Any additional parameters to add to the aggregator 936 * @return The aggregation outcomes 937 * @throws GeneralException if any IIQ failure occurs 938 */ 939 public AggregationOutcome aggregateAccount(Application appObject, Connector appConnector, ResourceObject resource, boolean refreshIdentity, Map<String, Object> aggregateArguments) throws GeneralException { 940 AggregateOptions options = new AggregateOptions(); 941 options.application = appObject; 942 options.applicationName = appObject.getName(); 943 options.connector = appConnector; 944 options.resourceObject = resource; 945 options.aggregateOptions = aggregateArguments; 946 options.refreshIdentity = refreshIdentity; 947 948 return aggregateAccount(options); 949 } 950 951 /** 952 * Aggregates the given account information into IIQ, given the Map as the resource object data. 953 * 954 * Identical to {@link #aggregateAccount(String, Map, Map)} with null for the final parameter. 955 * 956 * @param application The application name 957 * @param resource The data representing the account fields 958 * @throws GeneralException if any IIQ failure occurs 959 * @return The aggregation outcome 960 */ 961 public AggregationOutcome aggregateAccount(String application, Map<String, Object> resource) throws GeneralException { 962 return aggregateAccount(application, resource, null); 963 } 964 965 /** 966 * Aggregates the given account information into IIQ, given the Map as the resource object data. 967 * 968 * @param application The application name 969 * @param resource The data representing the account fields 970 * @param arguments Any arguments to pass to the Aggregator 971 * @throws GeneralException if any IIQ failure occurs 972 * @return The aggregation outcome 973 */ 974 public AggregationOutcome aggregateAccount(String application, Map<String, Object> resource, Map<String, Object> arguments) throws GeneralException { 975 ResourceObject resourceObject = new ResourceObject(); 976 resourceObject.setAttributes(new Attributes<>(resource)); 977 Application appObject = context.getObjectByName(Application.class, application); 978 if (appObject == null) { 979 throw new GeneralException("No such application: " + application); 980 } 981 982 // Fix the resource object Identity field 983 fixResourceObjectIdentity(resourceObject, appObject); 984 985 String appConnName = appObject.getConnector(); 986 Connector appConnector = ConnectorFactory.getConnector(appObject, null); 987 if (null == appConnector) { 988 throw new GeneralException("Failed to construct an instance of connector [" + appConnName + "]"); 989 } 990 991 AggregateOptions options = new AggregateOptions(); 992 options.application = appObject; 993 options.applicationName = appObject.getName(); 994 options.connector = appConnector; 995 options.resourceObject = resourceObject; 996 options.aggregateOptions = arguments; 997 options.runAppCustomization = true; 998 999 return aggregateAccount(options); 1000 } 1001 1002 /** 1003 * Aggregates the given account information into IIQ, given the native identity and application name. 1004 * Optionally, refresh the identity. 1005 * 1006 * Identical to {@link #aggregateAccount(String, String, boolean, boolean, Map)} with false for the 1007 * forceAggregate parameter and an empty Map for the final parameter. 1008 * 1009 * The Application in question must support the "random access" feature (i.e. it must *not* have the 1010 * NO_RANDOM_ACCESS flag defined) unless forceAggregate is true. 1011 * 1012 * @param application The application name to check 1013 * @param id The native identity on the target system 1014 * @param refreshIdentity If true, the identity will be refreshed after aggregation 1015 * @throws GeneralException if any IIQ failure occurs 1016 * @return The aggregation outcome 1017 */ 1018 public AggregationOutcome aggregateAccount(String application, String id, boolean refreshIdentity) throws GeneralException { 1019 return aggregateAccount(application, id, refreshIdentity, false, new HashMap<>()); 1020 } 1021 1022 /** 1023 * Aggregates the given account information into IIQ, given only a nativeIdentity. Additionally, 1024 * optionally refresh the user. 1025 * 1026 * Identical to {@link #aggregateAccount(String, String, boolean, boolean, Map)} with false for the 1027 * forceAggregate parameter. 1028 * 1029 * The Application in question must support the "random access" feature (i.e. it must *not* have 1030 * the NO_RANDOM_ACCESS flag defined). 1031 * 1032 * @param application The application name to check 1033 * @param id The native identity on the target system 1034 * @param refreshIdentity If true, the identity will be refreshed after aggregation 1035 * @param arguments Any optional arguments to pass to the Aggregator 1036 * @throws GeneralException if any IIQ failure occurs 1037 * @return The aggregation outcome 1038 */ 1039 public AggregationOutcome aggregateAccount(String application, String id, boolean refreshIdentity, Map<String, Object> arguments) throws GeneralException { 1040 return aggregateAccount(application, id, refreshIdentity, false, arguments); 1041 } 1042 1043 /** 1044 * Aggregates the given account information into IIQ, given only a nativeIdentity. 1045 * Additionally, optionally refresh the user. If the forceAggregate option is true, 1046 * the aggregation will be attempted even if the Application does not support 1047 * random access (e.g., DelimitedFile). 1048 * 1049 * Identical to {@link #aggregateAccount(String, String, boolean, boolean, Map)} with an empty Map 1050 * for the final parameter. 1051 * 1052 * @param application The application name to check 1053 * @param id The native identity on the target system 1054 * @param refreshIdentity If true, the identity will be refreshed after aggregation 1055 * @param forceAggregate If true, we may override what Sailpoint tells us about the features of certain applications 1056 * @throws GeneralException if any IIQ failure occurs 1057 * @return The aggregation outcome 1058 */ 1059 public AggregationOutcome aggregateAccount(String application, String id, boolean refreshIdentity, boolean forceAggregate) throws GeneralException { 1060 return aggregateAccount(application, id, refreshIdentity, forceAggregate, new HashMap<>()); 1061 } 1062 1063 /** 1064 * Aggregates the given account information into IIQ, given only a nativeIdentity and application name. 1065 * Additionally, optionally refresh the user. 1066 * 1067 * The Application in question must support the "random access" feature (i.e. it must *not* have the 1068 * NO_RANDOM_ACCESS flag defined) unless forceAggregate is true. 1069 * 1070 * @param application The application name to check 1071 * @param id The native identity on the target system 1072 * @param refreshIdentity If true, the identity will be refreshed after aggregation 1073 * @param forceAggregate If true, we will attempt aggregation anyway, even if the application does not support random access 1074 * @param arguments Any optional arguments to pass to the Aggregator 1075 * @throws GeneralException if any IIQ failure occurs 1076 * @return The aggregation outcome 1077 */ 1078 public AggregationOutcome aggregateAccount(String application, String id, boolean refreshIdentity, boolean forceAggregate, Map<String, Object> arguments) throws GeneralException { 1079 Application appObject = context.getObjectByName(Application.class, application); 1080 if (appObject == null) { 1081 throw new GeneralException("Invalid application name: " + application); 1082 } 1083 1084 String appConnName = appObject.getConnector(); 1085 Connector appConnector = ConnectorFactory.getConnector(appObject, null); 1086 if (null == appConnector) { 1087 throw new GeneralException("Failed to construct an instance of connector [" + appConnName + "]"); 1088 } 1089 1090 AggregateOptions options = new AggregateOptions(); 1091 options.applicationName = appObject.getName(); 1092 options.application = appObject; 1093 options.connector = appConnector; 1094 options.accountName = id; 1095 options.forceAggregate = forceAggregate; 1096 options.refreshIdentity = refreshIdentity; 1097 options.aggregateOptions = arguments; 1098 1099 return aggregateAccount(options); 1100 } 1101 1102 /** 1103 * Aggregates all accounts for the given identity, returning the same output as {@link #aggregateAccounts(MultipleAggregateOptions)}. 1104 * @param identity The identity ID or name 1105 * @return The outcomes of the aggregations 1106 * @throws GeneralException if anything fails 1107 */ 1108 public Map<Pair<String, String>, AggregationOutcome> aggregateAccounts(String identity) throws GeneralException { 1109 MultipleAggregateOptions options = new MultipleAggregateOptions(); 1110 options.identity = identity; 1111 return aggregateAccounts(options); 1112 } 1113 1114 /** 1115 * Aggregates a list of individual accounts belonging to an Identity according to the options provided. 1116 * This allows you to "resync" an Identity's entire set of existing links in a single operation. 1117 * 1118 * In the output Map, the key is a {@link Pair} with the first element being the application 1119 * name and the second element being the native identity. The Map value is the {@link AggregationOutcome} 1120 * corresponding to that aggregation. 1121 * 1122 * If a list of accounts is supplied, it will be used. Otherwise, all accounts on the Identity 1123 * will be aggregated, subject to the accountFilter if one is provided. 1124 * 1125 * The four-argument version of aggregateAccount() is used to perform each individual aggregation: 1126 * #aggregateAccount(String, String, boolean, Map). 1127 * 1128 * The aggregation option correlateOnly will always be set to true. 1129 * 1130 * @param options The options, specifying which Identity and accounts to aggregate 1131 * @return The outcomes of the aggregations 1132 * @throws GeneralException if any failures occur 1133 */ 1134 public Map<Pair<String, String>, AggregationOutcome> aggregateAccounts(MultipleAggregateOptions options) throws GeneralException { 1135 if (Util.isNullOrEmpty(options.identity)) { 1136 throw new IllegalArgumentException("The input options must contain an Identity name or ID"); 1137 } 1138 1139 Identity target = context.getObject(Identity.class, options.identity); 1140 if (target == null) { 1141 throw new IllegalArgumentException("The identity " + options.identity + " does not exist"); 1142 } 1143 1144 if (Util.isNotNullOrEmpty(options.accountFilterString) && options.accountFilter == null) { 1145 options.accountFilter = Filter.compile(options.accountFilterString); 1146 } 1147 1148 List<Pair<String, String>> toProcess = Util.isEmpty(options.getAccountsList()) ? new ArrayList<>() : options.getAccountsList(); 1149 1150 if (Util.isEmpty(toProcess)) { 1151 List<Link> accounts = target.getLinks(); 1152 1153 for (Link link : Util.safeIterable(accounts)) { 1154 boolean included = true; 1155 if (options.accountFilter != null) { 1156 HybridObjectMatcher matcher = new HybridObjectMatcher(context, options.accountFilter); 1157 included = matcher.matches(link); 1158 } 1159 1160 if (included) { 1161 toProcess.add(Pair.make(link.getApplicationName(), link.getNativeIdentity())); 1162 } 1163 } 1164 } 1165 1166 Map<Pair<String, String>, AggregationOutcome> linkOutcomes = new HashMap<>(); 1167 1168 for(Pair<String, String> pair : toProcess) { 1169 Map<String, Object> aggArgs = new HashMap<>(); 1170 aggArgs.put(Aggregator.ARG_CORRELATE_ONLY, true); 1171 1172 AggregationOutcome outcome = aggregateAccount(pair.getFirst(), pair.getSecond(), false, aggArgs); 1173 if (outcome != null) { 1174 linkOutcomes.put(pair, outcome); 1175 } 1176 } 1177 1178 return linkOutcomes; 1179 } 1180 1181 /** 1182 * Aggregates the given account in the background via the Aggregate Request request 1183 * type. Uses a slightly future event date to fire the request asynchronously. 1184 * 1185 * @param targetIdentity The target identity 1186 * @param application The application from which the account is being aggregated 1187 * @param ro The resource object to process asynchronously 1188 * @throws GeneralException on failures 1189 */ 1190 public void backgroundAggregateAccount(Identity targetIdentity, Application application, ResourceObject ro) throws GeneralException { 1191 Map<String, Object> resourceObject = new HashMap<>(ro.getAttributes()); 1192 Attributes<String, Object> requestParams = new Attributes<>(); 1193 requestParams.put(AggregateRequestExecutor.ARG_RESOURCE_OBJECT, resourceObject); 1194 requestParams.put(AggregateRequestExecutor.ARG_IDENTITY_NAME, Objects.requireNonNull(targetIdentity).getName()); 1195 requestParams.put(AggregateRequestExecutor.ARG_APP_NAME, Objects.requireNonNull(application).getName()); 1196 1197 Map<String, Object> aggregationOptions = new HashMap<>(); 1198 aggregationOptions.put(ServiceHandler.ARG_AGGREGATE_NO_RANDOM_ACCESS, true); 1199 requestParams.put(AggregateRequestExecutor.ARG_AGG_OPTIONS, aggregationOptions); 1200 1201 RequestDefinition requestDefinition = context.getObjectByName(RequestDefinition.class, AggregateRequestExecutor.DEF_NAME); 1202 1203 Request request = new Request(); 1204 request.setEventDate(new Date(System.currentTimeMillis() + 250)); 1205 request.setDefinition(requestDefinition); 1206 request.setAttributes(requestParams); 1207 1208 RequestManager.addRequest(context, request); 1209 } 1210 1211 /** 1212 * Creates the given account 1213 * @param user The user to add the account to 1214 * @param applicationName The application name 1215 * @param map The account data 1216 * @throws GeneralException If any failures occur 1217 */ 1218 public void createAccount(Identity user, String applicationName, Map<String, Object> map) throws GeneralException { 1219 ProvisioningPlan plan = new ProvisioningPlan(); 1220 1221 plan.setIdentity(user); 1222 1223 AccountRequest accountRequest = new AccountRequest(); 1224 accountRequest.setOperation(AccountRequest.Operation.Create); 1225 1226 for(String key : map.keySet()) { 1227 String provisioningName = key; 1228 ProvisioningPlan.Operation operation = ProvisioningPlan.Operation.Set; 1229 if (key.contains(":")) { 1230 String[] components = key.split(":"); 1231 operation = ProvisioningPlan.Operation.valueOf(components[0]); 1232 provisioningName = components[1]; 1233 } 1234 AttributeRequest request = new AttributeRequest(); 1235 request.setName(provisioningName); 1236 request.setOperation(operation); 1237 request.setValue(map.get(key)); 1238 accountRequest.add(request); 1239 } 1240 1241 plan.add(accountRequest); 1242 1243 Map<String, Object> extraParameters = new HashMap<>(); 1244 extraParameters.put("approvalScheme", "none"); 1245 this.provisioningUtilities.doProvisioning(user.getName(), plan, false, extraParameters); 1246 } 1247 1248 /** 1249 * Disables the given account in the target system 1250 * @param target The target to disable 1251 * @throws GeneralException if any IIQ failure occurs 1252 */ 1253 public void disable(Link target) throws GeneralException { 1254 Objects.requireNonNull(target, "A non-null Link must be provided"); 1255 new ProvisioningUtilities(context).disableAccount(target); 1256 } 1257 1258 /** 1259 * Retrieves a single record from a JDBC application, simulating a properly 1260 * working getObject(). 1261 * 1262 * The JDBC connector has a bug where the Connection object is not passed to 1263 * a BuildMap rule following a getObject(). This method works around the bug 1264 * by calling iterateObjects() instead after swapping out the getObjectSQL 1265 * and SQL parameters. 1266 * 1267 * NOTE: This is no longer necessary as of 8.2, as this bug has been fixed. 1268 * 1269 * TODO this does NOT work where a stored procedure is used. 1270 * 1271 * @param application The application to swap SQL and getObjectSQL 1272 * @param nativeIdentity The native identity to query 1273 * @return The queried ResourceObject 1274 * @throws GeneralException on failures to work with the Application 1275 * @throws ConnectorException on failures to work with the Connector 1276 */ 1277 public ResourceObject doJDBCConnectorHack(Application application, String nativeIdentity) throws GeneralException, ConnectorException { 1278 // The JDBC connector has a weird bug in getObject where BuildMap rules 1279 // are not passed the Connection to the target system. We will offer the 1280 // option to use the "iterate" function for "get object" by faking out the 1281 // query. Connection works fine for iterate. 1282 ResourceObject resourceObject = null; 1283 Application cloned = (Application) application.deepCopy((XMLReferenceResolver) context); 1284 cloned.clearPersistentIdentity(); 1285 String getObjectSQL = cloned.getAttributes().getString("getObjectSQL"); 1286 if (Util.isNotNullOrEmpty(getObjectSQL)) { 1287 Map<String, Object> variables = new HashMap<>(); 1288 variables.put("identity", nativeIdentity); 1289 getObjectSQL = Util.expandVariables(getObjectSQL, variables); 1290 cloned.getAttributes().put("SQL", getObjectSQL); 1291 1292 Connector sqlConnector = ConnectorFactory.getConnector(cloned, null); 1293 CloseableIterator<ResourceObject> results = sqlConnector.iterateObjects("account", null, new HashMap<>()); 1294 try { 1295 // This should produce only one result 1296 if (results.hasNext()) { 1297 resourceObject = results.next(); 1298 } 1299 } finally { 1300 results.close(); 1301 } 1302 } 1303 return resourceObject; 1304 } 1305 1306 /** 1307 * Retrieves a single account from the ServiceNow connector. 1308 * 1309 * The ServiceNow connector does not respect all of the connector options for 1310 * single-account (getObject) aggregation. This means that you end up with a 1311 * weird subset of fields. We need to do a "big" aggregation with the connector 1312 * filtered to a single account. 1313 * 1314 * @param field The field to query 1315 * @param id The value for that field (usually a sys_id) 1316 * @param appObject The Application 1317 * @param skipGroups If true, groups and roles will not be cached (or queried) 1318 * @return The resulting ResourceObject from the query 1319 * @throws GeneralException If any failures occur 1320 * @throws ConnectorException If any connector failures occur 1321 */ 1322 public ResourceObject doServiceNowConnectorHack(String field, String id, Application appObject, boolean skipGroups) throws GeneralException, ConnectorException { 1323 ResourceObject rObj = null; 1324 Application cloned = (Application) appObject.deepCopy((XMLReferenceResolver) context); 1325 cloned.clearPersistentIdentity(); 1326 if (skipGroups) { 1327 Schema schema = cloned.getSchema("account"); 1328 schema.clearPersistentIdentity(); 1329 schema.removeAttribute("groups"); 1330 schema.removeAttribute("roles"); 1331 } 1332 cloned.getAttributes().put("accountFilterAttribute", field + "=" + id); 1333 Connector snConnector = ConnectorFactory.getConnector(cloned, null); 1334 Filter userFilter = Filter.eq(field, id); 1335 CloseableIterator<ResourceObject> results = snConnector.iterateObjects("account", userFilter, new HashMap<>()); 1336 try { 1337 // This should produce only one result 1338 if (results.hasNext()) { 1339 rObj = results.next(); 1340 if (skipGroups) { 1341 // This means we only update attributes in the ResourceObject; we don't treat it as authoritative for all attributes 1342 rObj.setIncomplete(true); 1343 } 1344 } 1345 } finally { 1346 results.close(); 1347 } 1348 return rObj; 1349 } 1350 1351 /** 1352 * Enables the given account in the target system 1353 * @param target The target to enable 1354 * @throws GeneralException if any IIQ failure occurs 1355 */ 1356 public void enable(Link target) throws GeneralException { 1357 Objects.requireNonNull(target, "A non-null Link must be provided"); 1358 new ProvisioningUtilities(context).enableAccount(target); 1359 } 1360 1361 /** 1362 * Invokes the Identitizer to refresh the searchable Link attributes 1363 * @param theLink The link to refresh 1364 * @throws GeneralException if anything fails 1365 */ 1366 public void fixLinkSearchableAttributes(Link theLink) throws GeneralException { 1367 if (theLink == null) { 1368 throw new IllegalArgumentException("Must pass a non-null Link"); 1369 } 1370 1371 Identitizer identitizer = new Identitizer(context); 1372 identitizer.setPromoteAttributes(true); 1373 identitizer.refreshLink(theLink); 1374 } 1375 1376 /** 1377 * Gets the provisioning utilities object associated with this AccountUtilities 1378 * for modification. 1379 * 1380 * @return The ProvisioningUtilities 1381 */ 1382 public ProvisioningUtilities getProvisioningUtilities() { 1383 return provisioningUtilities; 1384 } 1385 1386 /** 1387 * Mask any attributes flagged as secret attributes at the ProvisioningPlan level, and also 1388 * any attributes that look like they might be secrets based on a set of likely substrings. 1389 * The list of tokens to check heuristically is stored in {@link #likelyPasswordTokens}. 1390 * 1391 * @param attributes The attribute map to modify 1392 */ 1393 public void heuristicMaskSecretAttributes(Map<String, Object> attributes) { 1394 if (attributes == null) { 1395 return; 1396 } 1397 maskSecretAttributes(attributes); 1398 List<String> toMask = new ArrayList<>(); 1399 for(String key : attributes.keySet()) { 1400 for(String token : likelyPasswordTokens) { 1401 if (key.toLowerCase().contains(token) && !key.toLowerCase().contains("expir")) { 1402 toMask.add(key); 1403 } 1404 } 1405 } 1406 for(String key : toMask) { 1407 attributes.put(key, "********"); 1408 } 1409 } 1410 1411 /** 1412 * Returns true if the given entitlement is assigned by a role. This will 1413 * first check the IdentityEntitlement metadata on the Identity and, failing 1414 * that, laboriously search through assigned and detected role metadata. 1415 * 1416 * NOTE: Why not just use IdentityEntitlements? Because they're a delayed indicator. 1417 * They are populated via specific refresh and aggregation flags and so may not 1418 * be up to date when you need this result. 1419 * 1420 * @param context A Sailpoint context 1421 * @param account The account to check 1422 * @param attribute The account attribute to examine 1423 * @param entitlementName The account attribute value to examine 1424 * @return True if the entitlement is associated with an assigned role 1425 * @throws GeneralException if any failures occur 1426 */ 1427 public boolean isAssignedByRole(SailPointContext context, Link account, String attribute, String entitlementName) throws GeneralException { 1428 boolean caseInsensitive = account.getApplication().isCaseInsensitive(); 1429 Identity who = account.getIdentity(); 1430 // Step 1: Find an IdentityEntitlement that matches 1431 QueryOptions qo = new QueryOptions(); 1432 qo.add(Filter.eq("identity.id", who.getId())); 1433 qo.add(Filter.eq("name", attribute)); 1434 qo.add(Filter.eq("application.id", account.getApplicationId())); 1435 if (caseInsensitive) { 1436 qo.add(Filter.ignoreCase(Filter.eq("value", entitlementName))); 1437 } else { 1438 qo.add(Filter.eq("value", entitlementName)); 1439 } 1440 1441 List<IdentityEntitlement> entitlements = context.getObjects(IdentityEntitlement.class, qo); 1442 for(IdentityEntitlement ie : Util.safeIterable(entitlements)) { 1443 if (ie.isGrantedByRole()) { 1444 return true; 1445 } 1446 } 1447 1448 // Step 2: If we got here, the IdentityEntitlement may simply have not been 1449 // assigned yet. We need to go spelunking through the roles to find it. 1450 for(RoleAssignment assignment : Util.safeIterable(who.getRoleAssignments())) { 1451 if (assignment.isNegative()) { 1452 continue; 1453 } 1454 for(RoleTarget target : Util.safeIterable(assignment.getTargets())) { 1455 if (target.getApplicationName().equals(account.getApplicationName()) && target.getNativeIdentity().equals(account.getNativeIdentity())) { 1456 for (AccountItem item : Util.safeIterable(target.getItems())) { 1457 if (item.getName().equals(attribute)) { 1458 List<String> valueList = item.getValueList(); 1459 if (valueList != null) { 1460 for(String v : valueList) { 1461 if (entitlementName.equals(v) || (caseInsensitive && entitlementName.equalsIgnoreCase(v))) { 1462 return true; 1463 } 1464 } 1465 } else if (item.getValue() != null) { 1466 Object v = item.getValue(); 1467 if (entitlementName.equals(v) || (caseInsensitive && entitlementName.equalsIgnoreCase(String.valueOf(v)))) { 1468 return true; 1469 } 1470 } 1471 } 1472 } 1473 } 1474 } 1475 } 1476 for(RoleDetection detection : Util.safeIterable(who.getRoleDetections())) { 1477 if (!detection.hasAssignmentIds()) { 1478 continue; 1479 } 1480 for(RoleTarget target : Util.safeIterable(detection.getTargets())) { 1481 if (target.getApplicationName().equals(account.getApplicationName()) && target.getNativeIdentity().equals(account.getNativeIdentity())) { 1482 for (AccountItem item : Util.safeIterable(target.getItems())) { 1483 if (item.getName().equals(attribute)) { 1484 List<String> valueList = item.getValueList(); 1485 if (valueList != null) { 1486 for(String v : valueList) { 1487 if (entitlementName.equals(v) || (caseInsensitive && entitlementName.equalsIgnoreCase(v))) { 1488 return true; 1489 } 1490 } 1491 } else if (item.getValue() != null) { 1492 Object v = item.getValue(); 1493 if (entitlementName.equals(v) || (caseInsensitive && entitlementName.equalsIgnoreCase(String.valueOf(v)))) { 1494 return true; 1495 } 1496 } 1497 } 1498 } 1499 } 1500 } 1501 } 1502 return false; 1503 } 1504 1505 /** 1506 * Mask any attributes flagged as secret attributes at the ProvisioningPlan level 1507 * @param attributes The attribute map to modify 1508 */ 1509 public void maskSecretAttributes(Map<String, Object> attributes) { 1510 if (attributes == null) { 1511 return; 1512 } 1513 List<String> secretAttributeNames = ProvisioningPlan.getSecretProvisionAttributeNames(); 1514 for(String attr : secretAttributeNames) { 1515 if (attributes.containsKey(attr)) { 1516 attributes.put(attr, "********"); 1517 } 1518 } 1519 } 1520 1521 /** 1522 * Runs the customization rule given the aggregate options. This will be invoked by 1523 * aggregateAccount at several different times. 1524 * 1525 * @param theRule The Customization rule to run 1526 * @param options The aggregate options container 1527 * @return The modified resource object 1528 */ 1529 private ResourceObject runCustomizationRule(Rule theRule, AggregateOptions options, AggregationOutcome outcome) { 1530 try { 1531 // Pass the mandatory arguments to the Customization rule for the app. 1532 Map<String, Object> ruleArgs = new HashMap<>(); 1533 ruleArgs.put("context", context); 1534 ruleArgs.put("log", log); 1535 ruleArgs.put("object", options.resourceObject); 1536 ruleArgs.put("application", options.application); 1537 ruleArgs.put("connector", options.connector); 1538 ruleArgs.put("state", new HashMap<String, Object>()); 1539 // Call the customization rule just like a normal aggregation would. 1540 Object output = context.runRule(theRule, ruleArgs, null); 1541 // Make sure we got a valid resourceObject back from the rule. 1542 if (output == null || output instanceof ResourceObject) { 1543 return (ResourceObject) output; 1544 } 1545 } catch (Exception e) { 1546 // Log and ignore 1547 log.error("Caught an error running Customization Rule " + theRule.getName(), e); 1548 outcome.addError("Caught an error running Customization Rule " + theRule.getName(), e); 1549 outcome.setStatus(OutcomeType.Failure); 1550 1551 return null; 1552 } 1553 return options.resourceObject; 1554 } 1555 1556}