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}