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