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