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