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