001package com.identityworksllc.iiq.common;
002
003import sailpoint.api.IdentityService;
004import sailpoint.api.ObjectUtil;
005import sailpoint.api.Provisioner;
006import sailpoint.api.SailPointContext;
007import sailpoint.api.Workflower;
008import sailpoint.object.*;
009import sailpoint.object.ProvisioningPlan.AccountRequest;
010import sailpoint.object.ProvisioningPlan.AttributeRequest;
011import sailpoint.provisioning.PlanCompiler;
012import sailpoint.tools.GeneralException;
013import sailpoint.tools.Message;
014import sailpoint.tools.Util;
015
016import java.time.Instant;
017import java.time.format.DateTimeFormatter;
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Objects;
023import java.util.Optional;
024import java.util.function.Consumer;
025import java.util.function.Predicate;
026import java.util.stream.Collectors;
027
028/**
029 * Utilities to wrap the several provisioning APIs available in SailPoint.
030 */
031@SuppressWarnings("unused")
032public class ProvisioningUtilities extends AbstractBaseUtility {
033
034        /**
035         * The attribute for provisioning assigned roles
036         */
037        public static final String ASSIGNED_ROLES_ATTR = "assignedRoles";
038
039        /**
040         * The constant to use for no approvals
041         */
042        public static final String NO_APPROVAL_SCHEME = "none";
043
044        /**
045         * The approval scheme workflow parameter
046         */
047        public static final String PLAN_PARAM_APPROVAL_SCHEME = "approvalScheme";
048
049        /**
050         * The notification scheme workflow parameter
051         */
052        public static final String PLAN_PARAM_NOTIFICATION_SCHEME = "notificationScheme";
053
054        /**
055         * Modifies the plan to add the given user to the given role, associating it statically
056         * with the given target accounts (or new accounts if none are specified).
057         *
058         * @param context the IIQ context
059         * @param identityName The identity name to add to the given role
060         * @param roleName The role to add
061         * @param targets These Links will be used as a provisioning target for the plan. If a value is null, a new account create will be requested.
062         * @param provisioningPlan The provisioning plan to modify
063         * @throws GeneralException if a failure occurs while looking up required objects
064         */
065        public static void addUserRolePlan(SailPointContext context, String identityName, String roleName, Map<String, Link> targets, ProvisioningPlan provisioningPlan) throws GeneralException {
066                Objects.requireNonNull(identityName);
067                Objects.requireNonNull(roleName);
068
069                // We have to generate our own assignment ID in this case
070                String assignmentKey = Util.uuid();
071                Bundle role = context.getObjectByName(Bundle.class, roleName);
072
073                if (role == null) {
074                        throw new IllegalArgumentException("Role " + roleName + " does not exist");
075                }
076
077                Identity planIdentity = provisioningPlan.getIdentity();
078
079                AccountRequest accountRequest = new AccountRequest(AccountRequest.Operation.Modify, ProvisioningPlan.APP_IIQ, null, planIdentity.getName());
080                AttributeRequest attributeRequest = new AttributeRequest(ASSIGNED_ROLES_ATTR, ProvisioningPlan.Operation.Add, roleName);
081                attributeRequest.setAssignmentId(assignmentKey);
082                accountRequest.add(attributeRequest);
083                provisioningPlan.add(accountRequest);
084
085                List<ProvisioningTarget> provisioningTargetSelectors = new ArrayList<>();
086
087                for(String appName : targets.keySet()) {
088                        Application application = context.getObjectByName(Application.class, appName);
089                        if (application == null) {
090                                throw new IllegalArgumentException("Application " + appName + " passed in the target map does not exist");
091                        }
092                        AccountSelection selection = new AccountSelection(application);;
093                        Link target = targets.get(appName);
094                        if (target == null) {
095                                selection.setAllowCreate(true);
096                                selection.setRoleName(roleName);
097                                selection.setDoCreate(true);
098                        } else {
099                                RoleTarget roleTarget = new RoleTarget(target);
100                                selection.setAllowCreate(false);
101                                selection.setRoleName(roleName);
102                                selection.addAccountInfo(target);
103                                selection.setSelection(roleTarget.getNativeIdentity());
104                        }
105                        ProvisioningTarget provisioningTarget = new ProvisioningTarget(assignmentKey, role);
106                        provisioningTarget.setRole(roleName);
107                        provisioningTarget.setApplication(appName);
108                        provisioningTarget.addAccountSelection(selection);
109                        provisioningTargetSelectors.add(provisioningTarget);
110                }
111
112                provisioningPlan.setProvisioningTargets(provisioningTargetSelectors);
113        }
114
115        /**
116         * Gets the arguments for the given plan, creating one if needed
117         * @param plan The request
118         * @return The arguments for the plan
119         */
120        public static Attributes<String, Object> getArguments(ProvisioningPlan plan) {
121                Attributes<String, Object> arguments = plan.getArguments();
122                if (arguments == null) {
123                        arguments = new Attributes<>();
124                        plan.setArguments(arguments);
125                }
126                return arguments;
127        }
128
129        /**
130         * Gets the arguments for the given request, creating one if needed
131         * @param request The request
132         * @return The arguments for the request
133         */
134        public static Attributes<String, Object> getArguments(ProvisioningPlan.AbstractRequest request) {
135                Attributes<String, Object> arguments = request.getArguments();
136                if (arguments == null) {
137                        arguments = new Attributes<>();
138                        request.setArguments(arguments);
139                }
140                return arguments;
141        }
142
143        /**
144         * Gets the IIQ account request from the given plan, creating one if needed
145         * @param plan The plan in question
146         * @return The IIQ account request
147         */
148        public static AccountRequest getIIQAccountRequest(ProvisioningPlan plan) {
149                AccountRequest accountRequest = plan.getIIQAccountRequest();
150                if (accountRequest == null) {
151                        accountRequest = new AccountRequest();
152                        accountRequest.setOperation(AccountRequest.Operation.Modify);
153                        accountRequest.setApplication(ProvisioningPlan.APP_IIQ);
154                        plan.add(accountRequest);
155                }
156                return accountRequest;
157        }
158
159        /**
160         * Intended to be used in a pre or post-provision rule, this method will return the given attribute from the AccountRequest if present and otherwise will return it from the Link. The Link will be looked up based on the contents of the AccountRequest.
161         * @param req The AccountRequest modifying this Link
162         * @param name The name of the attribute to return
163         * @return the attribute value
164         * @throws GeneralException if a query failure occurs
165         */
166        public static Object getLatestValue(SailPointContext context, AccountRequest req, String name) throws GeneralException {
167        Link account = getLinkFromRequest(context, req);
168        return getLatestValue(account, req, name);
169        }
170
171        /**
172         * Intended to be used in a pre or post-provision rule, this method will return the given attribute from the AccountRequest if present and otherwise will return it from the Link
173         * @param account The Link being modified
174         * @param req The AccountRequest modifying this Link
175         * @param name The name of the attribute to return
176         * @return the attribute value
177         */
178        public static Object getLatestValue(Link account, AccountRequest req, String name) {
179                Object value = null;
180
181                AttributeRequest attr = req.getAttributeRequest(name);
182                if (attr != null) {
183                        value = attr.getValue();
184                } else if (account != null) {
185                        value = account.getAttributes().get(name);
186                }
187                return value;
188        }
189
190        /**
191         * Retrieves the Link that is associated with the given AccountRequest, or returns null
192         * if no link can be found. The Application on the request must be set and accurate.
193         *
194         * On create, the outcome will always be null because the Link doesn't exist until after
195         * the operation has completed.
196         *
197         * @param request The request to use to search
198         * @return the matching Link, or null if none can be found
199         * @throws GeneralException if more than one matching Link is found
200         */
201        public static Link getLinkFromRequest(SailPointContext context, AccountRequest request) throws GeneralException {
202                if (request == null || Util.isNullOrEmpty(request.getApplication()) || Util.nullSafeEq(request.getApplication(), ProvisioningPlan.APP_IIQ)) {
203                        return null;
204                }
205                Application app = request.getApplication(context);
206                if (app != null) {
207                        Filter filter = Filter.and(Filter.eq("application.name", app.getName()), Filter.eq("nativeIdentity", request.getNativeIdentity()));
208                        QueryOptions qo = new QueryOptions();
209                        qo.add(filter);
210
211                        List<Link> objects = context.getObjects(Link.class, qo);
212                        if (objects.size() == 1) {
213                                return objects.get(0);
214                        } else if (objects.size() == 0) {
215                                return null;
216                        } else {
217                                String message = "Expected to find 1 Link with application = " + app.getName() + " and native identity = " + request.getNativeIdentity() + ", but found " + objects.size();
218                                throw new GeneralException(message);
219                        }
220                }
221                return null;
222        }
223
224        /**
225         * Creates an AttributeRequest to move the given Link to the given target Identity,
226         * either modifying the provided plan or creating a new one.
227         *
228         * A move-account plan can be structured in either direction. It can be an "Add"
229         * plan that focuses on the destination Identity (allowing movement of accounts
230         * from more than one source) or a "Remove" plan that focuses on the source Identity
231         * (allowing movement of accounts to more than one target). You cannot mix these
232         * on a single plan.
233         *
234         * If the plan does not already contain a link move, it will be set up as an Add.
235         *
236         * This method will throw an exception if you pass an existing plan and its structure
237         * does not match the objects you pass in.
238         *
239         * @param theLinkToMove The link to move
240         * @param targetIdentity The Identity to which the link should be moved
241         * @param existingPlan The existing plan to modify, or null to create a new one
242         * @return The plan created or modified by this method
243         * @throws GeneralException if any validation failures occur
244         */
245        @SuppressWarnings("unused")
246        public static ProvisioningPlan linkMovePlan(Link theLinkToMove, Identity targetIdentity, ProvisioningPlan existingPlan) throws GeneralException {
247                if (theLinkToMove == null) {
248                        throw new GeneralException("The link to move cannot be null");
249                }
250
251                if (targetIdentity == null || Util.isNullOrEmpty(targetIdentity.getId())) {
252                        throw new GeneralException("The target Identity must be not be null and must have an ID");
253                }
254
255                Identity sourceIdentity = theLinkToMove.getIdentity();
256                if (sourceIdentity == null) {
257                        throw new GeneralException("Link " + theLinkToMove.getId() + " does not appear to have a valid Identity attached??");
258                }
259
260                if (Util.nullSafeEq(sourceIdentity.getId(), targetIdentity.getId())) {
261                        throw new GeneralException("Source and target Identity are the same");
262                }
263
264                // Add or Remove, depending on how the existing plan is structured (default Add)
265                ProvisioningPlan.Operation op;
266
267                // The identity ID we expect to find in the existing IIQ AccountRequest, if one exists
268                String expectedAccountRequestIdentity;
269
270                ProvisioningPlan thePlan = existingPlan;
271                if (thePlan == null) {
272                        thePlan = new ProvisioningPlan();
273                        thePlan.setComments("Move account " + theLinkToMove.getId() + " via API");
274                }
275
276                if (thePlan.getIdentity() == null) {
277                        // Move-to plan
278                        thePlan.setIdentity(targetIdentity);
279                        op = ProvisioningPlan.Operation.Add;
280                        expectedAccountRequestIdentity = targetIdentity.getId();
281                } else {
282                        Identity existingIdentity = thePlan.getIdentity();
283                        if (Util.nullSafeEq(existingIdentity.getId(), sourceIdentity.getId())) {
284                                // Move-from plan
285                                op = ProvisioningPlan.Operation.Remove;
286                                expectedAccountRequestIdentity = sourceIdentity.getId();
287                        } else if (Util.nullSafeEq(existingIdentity.getId(), targetIdentity.getId())) {
288                                // Move-to plan
289                                op = ProvisioningPlan.Operation.Add;
290                                expectedAccountRequestIdentity = targetIdentity.getId();
291                        } else {
292                                // We don't know who this is, error
293                                throw new GeneralException("The ProvisioningPlan's associated Identity is neither the specified source nor target; cannot construct a link move plan");
294                        }
295                }
296
297                AccountRequest iiqRequest = thePlan.getIIQAccountRequest();
298                if (iiqRequest == null) {
299                        iiqRequest = new AccountRequest();
300                        iiqRequest.setOperation(AccountRequest.Operation.Modify);
301                        iiqRequest.setApplication(ProvisioningPlan.APP_IIQ);
302                        iiqRequest.setNativeIdentity(expectedAccountRequestIdentity);
303
304                        thePlan.add(iiqRequest);
305                } else {
306                        String nativeIdentity = iiqRequest.getNativeIdentity();
307                        if (!Util.nullSafeEq(nativeIdentity, expectedAccountRequestIdentity)) {
308                                // We don't know who this is, error
309                                throw new GeneralException("The plan's 'IIQ' AccountRequest already has target Identity [" + nativeIdentity + "], which is not the same as the expected Identity ID [" + expectedAccountRequestIdentity + "]. A move-account plan can only move to one Identity or from one Identity.");
310                        }
311                }
312
313                AttributeRequest attributeRequest = new AttributeRequest(ProvisioningPlan.ATT_IIQ_LINKS, op, theLinkToMove.getId());
314                if (op == ProvisioningPlan.Operation.Add) {
315                        // Move from this source
316                        attributeRequest.put(ProvisioningPlan.ARG_SOURCE_IDENTITY, sourceIdentity.getId());
317                } else {
318                        // Move to this target
319                        attributeRequest.put(ProvisioningPlan.ARG_DESTINATION_IDENTITY, targetIdentity.getId());
320                }
321                iiqRequest.add(attributeRequest);
322
323                return thePlan;
324        }
325
326        /**
327         * Modifies the plan to add a role removal request for the given role
328         * @param roleName The role to remove from the identity
329         * @param revoke If true, the role will be revoked and not removed
330         * @param provisioningPlan The plan to add the role removal to
331         * @throws GeneralException If a failure occurs
332         */
333        public static void removeUserRolePlan(String roleName, boolean revoke, ProvisioningPlan provisioningPlan) throws GeneralException {
334                Objects.requireNonNull(roleName);
335                if (roleName.trim().isEmpty()) {
336                        throw new IllegalArgumentException("A non-empty role name must be provided");
337                }
338                Objects.requireNonNull(provisioningPlan);
339                AccountRequest accountRequest = provisioningPlan.getIIQAccountRequest();
340                if (accountRequest == null) {
341                        accountRequest = new AccountRequest();
342                        provisioningPlan.add(accountRequest);
343                }
344                accountRequest.setOperation(AccountRequest.Operation.Modify);
345                accountRequest.setApplication(ProvisioningPlan.APP_IIQ);
346                AttributeRequest attributeRequest = new AttributeRequest();
347                attributeRequest.setOperation(revoke ? ProvisioningPlan.Operation.Revoke : ProvisioningPlan.Operation.Remove);
348                attributeRequest.setName(ASSIGNED_ROLES_ATTR);
349                attributeRequest.setValue(roleName);
350                List<AttributeRequest> requestList = new ArrayList<>();
351                requestList.add(attributeRequest);
352                accountRequest.setAttributeRequests(requestList);
353                provisioningPlan.add(accountRequest);
354        }
355
356        /**
357         * Creates a AccountSelection object from the given account selection
358         * @param target The target to transform
359         * @return An {@link AccountSelection} with the given RoleTarget parameters
360         */
361        public static AccountSelection roleTargetToAccountSelection(RoleTarget target) {
362                AccountSelection selection = new AccountSelection();
363                selection.addAccountInfo(target);
364                return selection;
365        }
366
367        /**
368         * Creates a Provisioning Target from account
369         * @param role The role being provisioned
370         * @param target The target account
371         * @return A ProvisioningTarget object for the given role / account combination
372         */
373        public static ProvisioningTarget toProvisioningTarget(Bundle role, Link target) {
374                String assignmentKey = Util.uuid();
375                AccountSelection selection = new AccountSelection(target.getApplication());
376                RoleTarget roleTarget = new RoleTarget(target);
377                selection.setAllowCreate(false);
378                selection.setRoleName(role.getName());
379                selection.addAccountInfo(target);
380                selection.setSelection(roleTarget.getNativeIdentity());
381                ProvisioningTarget provisioningTarget = new ProvisioningTarget(assignmentKey, role);
382                provisioningTarget.setRole(role.getName());
383                provisioningTarget.setApplication(target.getApplicationName());
384                provisioningTarget.addAccountSelection(selection);
385                return provisioningTarget;
386        }
387
388        /**
389         * Creates a Provisioning Target from the given application and nativeIdentity name
390         * @param role The role being provisioned
391         * @param application The application to target
392         * @param nativeIdentity The native identity to target
393         * @return A ProvisioningTarget object for the given role / account combination
394         * @throws GeneralException if any failures occur
395         */
396        public static ProvisioningTarget toProvisioningTarget(SailPointContext context, Bundle role, String application, String nativeIdentity) throws GeneralException {
397                String assignmentKey = Util.uuid();
398                Application applicationObj = context.getObjectByName(Application.class, application);
399                AccountSelection selection = new AccountSelection(applicationObj);
400                RoleTarget roleTarget = new RoleTarget();
401                roleTarget.setApplicationName(application);
402                roleTarget.setRoleName(role.getName());
403                if (Util.isNotNullOrEmpty(nativeIdentity)) {
404                        roleTarget.setNativeIdentity(nativeIdentity);
405                } else {
406                        selection.setAllowCreate(true);
407                }
408                selection.setRoleName(role.getName());
409                selection.addAccountInfo(roleTarget);
410                selection.setSelection(nativeIdentity);
411                ProvisioningTarget provisioningTarget = new ProvisioningTarget(assignmentKey, role);
412                provisioningTarget.setRole(role.getName());
413                provisioningTarget.setApplication(application);
414                provisioningTarget.addAccountSelection(selection);
415                return provisioningTarget;
416        }
417        /**
418         * Invoked in the final tier of doProvisioning if present, mainly for testing purposes
419         */
420        private Consumer<ProvisioningPlan> beforeProvisioningConsumer;
421        /**
422         * If not null, this ticket ID will be attached to any provisioning operation
423         */
424        private String externalTicketId;
425        /**
426         * Any plan arguments passed to the provisioner
427         */
428        private final Attributes<String, Object> planArguments;
429        /**
430         * Invoked with the compiled project just before provisioning
431         */
432        private Consumer<ProvisioningProject> projectDebugger;
433        /**
434         * The workflow configuration container
435         */
436        private final ProvisioningArguments provisioningArguments;
437        /**
438         * Invoked with the compiled project just before provisioning
439         */
440        private Consumer<WorkflowLaunch> workflowDebugger;
441
442        /**
443         * Constructs a workflow-based Provisioning Utilities that will use the default
444         * LCM Provisioning workflow for all operations.
445         *
446         * @param c The SailPoint context
447         */
448        public ProvisioningUtilities(SailPointContext c) {
449                super(Objects.requireNonNull(c));
450                provisioningArguments = new ProvisioningArguments();
451                planArguments = new Attributes<>();
452        }
453
454        public ProvisioningUtilities(SailPointContext context, ProvisioningArguments arguments) {
455                this(context);
456
457                if (arguments != null) {
458                        this.provisioningArguments.merge(arguments);
459                }
460        }
461
462        @SuppressWarnings("unchecked")
463        public ProvisioningUtilities(SailPointContext context, Map<String, Object> arguments) throws GeneralException {
464                this(context);
465
466                if (arguments == null) {
467                        throw new IllegalArgumentException("Invalid input to ProvisioningUtilities: cannot provide a null Map for arguments");
468                }
469
470                Attributes<String, Object> map = new Attributes<>(arguments);
471
472                provisioningArguments.setErrorOnAccountSelection(map.getBoolean("errorOnAccountSelection"));
473                provisioningArguments.setErrorOnManualTask(map.getBoolean("errorOnManualTask"));
474                provisioningArguments.setErrorOnNewAccount(map.getBoolean("errorOnNewAccount"));
475                provisioningArguments.setErrorOnProvisioningForms(map.getBoolean("errorOnProvisioningForms"));
476
477                this.externalTicketId = map.getString("externalTicketId");
478
479                provisioningArguments.setPlanFieldName(map.getString("workflowPlanField"));
480                provisioningArguments.setIdentityFieldName(map.getString("workflowIdentityField"));
481                provisioningArguments.setCaseNameTemplate(map.getString("caseNameTemplate"));
482                if (map.get("provisioningWorkflow") instanceof String) {
483                        provisioningArguments.setWorkflowName(map.getString("provisioningWorkflow"));
484                }
485                provisioningArguments.setUseWorkflow(map.getBoolean("useWorkflow", true));
486                if (map.get("defaultExtraParameters") instanceof Map) {
487                        provisioningArguments.setDefaultExtraParameters((Map<String, Object>) map.get("defaultExtraParameters"));
488                }
489                if (map.get("planArguments") instanceof Map) {
490                        Map<String, Object> tempMap = (Map<String, Object>) map.get("planArguments");
491                        this.planArguments.putAll(tempMap);
492                }
493        }
494
495        /**
496         * Constructs a Provisioning Utilities that will optionally directly forward provisioning operations to the Provisioner.
497         * @param c The SailPoint context
498         * @param useWorkflow If true, workflows will be bypassed and provisioning will be sent directly to the provisioner
499         */
500        public ProvisioningUtilities(SailPointContext c, boolean useWorkflow) {
501                this(c);
502                provisioningArguments.setUseWorkflow(true);
503        }
504
505        /**
506         * Constructs a workflow-based Provisioning Utilities that will use the given workflow instead of the default
507         * @param c The SailPoint context
508         * @param provisioningWorkflowName The name of a provisioning workflow which should expect an 'identityName' and 'plan' attribute
509         */
510        public ProvisioningUtilities(SailPointContext c, String provisioningWorkflowName) {
511                this(c, true);
512                provisioningArguments.setWorkflowName(provisioningWorkflowName);
513        }
514
515        /**
516         * Constructs a Provisioning Utilities that will optionally directly forward provisioning operations to the Provisioner, or else will use the given provisioning workflow
517         * @param c The SailPoint context
518         * @param provisioningWorkflowName The name of a provisioning workflow which should expect an 'identityName' and 'plan' attribute
519         * @param useWorkflow If true, workflows will be bypassed and provisioning will be sent directly to the provisioner
520         */
521        public ProvisioningUtilities(SailPointContext c, String provisioningWorkflowName, boolean useWorkflow) {
522                this(c, provisioningWorkflowName);
523                provisioningArguments.setUseWorkflow(useWorkflow);
524        }
525
526        /**
527         * Adds the given entitlement to the given account on the user
528         * @param identityName The identity name
529         * @param account The account to modify
530         * @param entitlement The managed attribute from which to extract the entitlement
531         * @param withApproval If false, approval will be skipped
532         * @throws GeneralException if any failure occurs
533         */
534        public void addEntitlement(String identityName, Link account, ManagedAttribute entitlement, boolean withApproval) throws GeneralException {
535                ProvisioningPlan plan = new ProvisioningPlan();
536                Identity identity = findIdentity(identityName);
537                plan.setIdentity(identity);
538
539                AccountRequest accountRequest = new AccountRequest(AccountRequest.Operation.Modify, account.getApplicationName(), account.getInstance(), account.getNativeIdentity());
540                plan.add(accountRequest);
541
542                AttributeRequest attributeRequest = new AttributeRequest(entitlement.getName(), ProvisioningPlan.Operation.Add, entitlement.getValue());
543                accountRequest.add(attributeRequest);
544
545                Map<String, Object> options = new HashMap<>();
546                if (!withApproval) {
547                        options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
548                }
549
550                doProvisioning(identityName, plan, false, options);
551        }
552
553        /**
554         * Adds the given entitlement to the given account on the user
555         * @param identityName The identity name
556         * @param account The account to modify
557         * @param attribute The attribute to modify
558         * @param value The value to add
559         * @param withApproval If false, approval will be skipped
560         * @throws GeneralException if any failure occurs
561         */
562        public void addEntitlement(String identityName, Link account, String attribute, String value, boolean withApproval) throws GeneralException {
563                ProvisioningPlan plan = new ProvisioningPlan();
564                Identity identity = findIdentity(identityName);
565                plan.setIdentity(identity);
566
567                AccountRequest accountRequest = new AccountRequest(AccountRequest.Operation.Modify, account.getApplicationName(), account.getInstance(), account.getNativeIdentity());
568                plan.add(accountRequest);
569
570                AttributeRequest attributeRequest = new AttributeRequest(attribute, ProvisioningPlan.Operation.Add, value);
571                accountRequest.add(attributeRequest);
572
573                Map<String, Object> options = new HashMap<>();
574                if (!withApproval) {
575                        options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
576                }
577
578                doProvisioning(identityName, plan, false, options);
579        }
580
581        /**
582         * Adds an argument to the ProvisioningPlan that will eventually be constructed
583         * on a call to {@link #doProvisioning(ProvisioningPlan)}
584         * @param argument The argument to add to the plan
585         * @param value The value to add to the plan
586         */
587        public void addPlanArgument(String argument, Object value) {
588                planArguments.put(argument, value);
589        }
590
591        /**
592         * Adds the given user to the given role, guessing the target account by name. If the
593         * plan expands to more than one account selection question, this method will throw
594         * an exception.
595         *
596         * @param identityName The identity name to add to the given role
597         * @param roleName The role to add
598         * @param withApproval If true, default approval will be required
599         * @param accountName The target account to locate
600         * @throws GeneralException if a provisioning failure occurs
601         */
602        public void addUserRole(String identityName, String roleName, boolean withApproval, String accountName) throws GeneralException {
603                Objects.requireNonNull(identityName, "The passed identityName must not be null");
604                Objects.requireNonNull(roleName, "The passed roleName must not be null");
605                if (Util.isNullOrEmpty(accountName)) {
606                        // No sense going through the below if we didn't pass an account name
607                        addUserRole(identityName, roleName, withApproval);
608                        return;
609                }
610                Bundle role = context.getObjectByName(Bundle.class, roleName);
611                if (role == null) {
612                        throw new IllegalArgumentException("Role " + roleName + " does not exist");
613                }
614                // We have to generate our own assignment ID in this case
615                String assignmentKey = Util.uuid();
616                ProvisioningPlan provisioningPlan = new ProvisioningPlan();
617                Identity identity = findIdentity(identityName);
618                provisioningPlan.setIdentity(identity);
619                AccountRequest accountRequest = new AccountRequest(AccountRequest.Operation.Modify, ProvisioningPlan.APP_IIQ, null, identity.getName());
620                AttributeRequest attributeRequest = new AttributeRequest(ASSIGNED_ROLES_ATTR, ProvisioningPlan.Operation.Add, roleName);
621                attributeRequest.setAssignmentId(assignmentKey);
622                accountRequest.add(attributeRequest);
623                provisioningPlan.add(accountRequest);
624
625                PlanCompiler compiler = new PlanCompiler(context);
626                ProvisioningProject project = compiler.compile(new Attributes<>(), provisioningPlan, new Attributes<>());
627                List<ProvisioningTarget> targets = Utilities.safeStream(project.getProvisioningTargets()).filter(ProvisioningTarget::isAnswered).collect(Collectors.toList());
628                if (targets.size() > 1) {
629                        throw new IllegalStateException("The resulting provisioning project has more than one unanswered account selection");
630                } else if (targets.size() == 1) {
631                        ProvisioningTarget tempTarget = targets.get(0);
632                        String appName = tempTarget.getApplication();
633                        Application application = context.getObjectByName(Application.class, appName);
634                        IdentityService ids = new IdentityService(context);
635                        List<Link> possibleTargets = ids.getLinks(identity, application);
636                        Optional<Link> maybeLink =
637                                        Utilities.safeStream(possibleTargets)
638                                                        .filter(Functions.isNativeIdentity(accountName))
639                                                        .findAny();
640
641                        List<ProvisioningTarget> provisioningTargetSelectors = new ArrayList<>();
642
643                        AccountSelection selection = new AccountSelection(application);
644                        if (!maybeLink.isPresent()) {
645                                selection.setAllowCreate(true);
646                                selection.setRoleName(roleName);
647                                selection.setDoCreate(true);
648                        } else {
649                                RoleTarget roleTarget = new RoleTarget(maybeLink.get());
650                                selection.setAllowCreate(false);
651                                selection.setRoleName(roleName);
652                                selection.addAccountInfo(roleTarget);
653                                selection.setSelection(roleTarget.getNativeIdentity());
654                        }
655                        ProvisioningTarget provisioningTarget = new ProvisioningTarget(assignmentKey, role);
656                        provisioningTarget.setRole(roleName);
657                        provisioningTarget.setApplication(appName);
658                        provisioningTarget.addAccountSelection(selection);
659                        provisioningTargetSelectors.add(provisioningTarget);
660                        provisioningPlan.setProvisioningTargets(provisioningTargetSelectors);
661                }
662                Map<String, Object> options = new HashMap<>();
663                if (!withApproval) {
664                        options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
665                }
666                doProvisioning(identity.getName(), provisioningPlan, false, options);
667        }
668
669        /**
670         * Adds the given user to the given role, associating it statically with the given
671         * target accounts (or new accounts if none are specified). If a target is not supplied
672         * for a given application that is provisioned by this role, the provisioning engine
673         * will automatically run any account selection rule followed by an attempt at heuristic
674         * guessing.
675         *
676         * @param identityName The identity name to add to the given role
677         * @param roleName The role to add
678         * @param withApproval If true, default approval will be required
679         * @param targets These Links will be used as a provisioning target for the plan. If a value is null, a new account create will be requested.
680         * @throws GeneralException if a provisioning failure occurs
681         */
682        public void addUserRole(String identityName, String roleName, boolean withApproval, Map<String, Link> targets) throws GeneralException {
683                Objects.requireNonNull(identityName);
684
685                ProvisioningPlan provisioningPlan = new ProvisioningPlan();
686                Identity identity = findIdentity(identityName);
687                provisioningPlan.setIdentity(identity);
688
689                addUserRolePlan(context, identityName, roleName, targets, provisioningPlan);
690
691                Map<String, Object> options = new HashMap<>();
692                if (!withApproval) {
693                        options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
694                }
695                doProvisioning(identity.getName(), provisioningPlan, false, options);
696        }
697
698        /**
699         * Adds the given user to the given role
700         * @param identityName The identity name to add to the given role
701         * @param roleName The role to add
702         * @throws GeneralException if a provisioning failure occurs
703         */
704        public void addUserRole(String identityName, String roleName) throws GeneralException {
705                addUserRole(identityName, roleName, false);
706        }
707
708        /**
709         * Adds the given user to the given role
710         * @param identityName The identity name to add to the given role
711         * @param roleName The role to add
712         * @param withApproval If true, default approval will be required
713         * @throws GeneralException if a provisioning failure occurs
714         */
715        public void addUserRole(String identityName, String roleName, boolean withApproval) throws GeneralException {
716        ProvisioningPlan provisioningPlan = new ProvisioningPlan();
717        Identity identity = findIdentity(identityName);
718        provisioningPlan.setIdentity(identity);
719        // This is magical
720        provisioningPlan.add(ProvisioningPlan.APP_IIQ, identity.getName(), ASSIGNED_ROLES_ATTR, ProvisioningPlan.Operation.Add, roleName);
721        Map<String, Object> options = new HashMap<>();
722        if (!withApproval) {
723            options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
724        }
725        doProvisioning(identity.getName(), provisioningPlan, false, options);
726    }
727
728        /**
729         * Adds an argument to the workflow or Provisioner that will be used in a call to
730         * {@link #doProvisioning(ProvisioningPlan)}. If the value provided is null, the
731         * key will be removed from the arguments.
732         *
733         * @param argument The argument to set the value for
734         * @param value The value to set
735         */
736        public void addWorkflowArgument(String argument, Object value) {
737                if (value == null) {
738                        provisioningArguments.getDefaultExtraParameters().remove(argument);
739                } else {
740                        provisioningArguments.getDefaultExtraParameters().put(argument, value);
741                }
742        }
743
744        /**
745         * Deletes the given account by submitting a Delete request to IIQ
746         * @param link The Link to disable
747         * @throws GeneralException if any failures occur
748         */
749        public void deleteAccount(Link link) throws GeneralException {
750                Objects.requireNonNull(link, "Provided Link must not be null");
751                ProvisioningPlan plan = new ProvisioningPlan();
752                String username = ObjectUtil.getIdentityFromLink(context, link.getApplication(), link.getInstance(), link.getNativeIdentity());
753                Identity user = findIdentity(username);
754                plan.setIdentity(user);
755                plan.add(link.getApplicationName(), link.getNativeIdentity(), AccountRequest.Operation.Delete);
756                Map<String, Object> extraParameters = new HashMap<>();
757                extraParameters.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
758                doProvisioning(user.getName(), plan, false, extraParameters);
759        }
760
761        /**
762         * Disables the given account by submitting a Disable request to IIQ
763         * @param link The Link to disable
764         * @throws GeneralException if any failures occur
765         */
766    public void disableAccount(Link link) throws GeneralException {
767                Objects.requireNonNull(link, "Provided Link must not be null");
768                disableAccount(link, false);
769        }
770
771        /**
772         * Disables the given account by submitting a Disable request to IIQ
773         * @param link The Link to disable
774         * @throws GeneralException if any failures occur
775         */
776        public void disableAccount(Link link, boolean doRefresh) throws GeneralException {
777                Objects.requireNonNull(link, "Provided Link must not be null");
778                ProvisioningPlan plan = new ProvisioningPlan();
779                String username = ObjectUtil.getIdentityFromLink(context, link.getApplication(), link.getInstance(), link.getNativeIdentity());
780                Identity user = findIdentity(username);
781                plan.setIdentity(user);
782                plan.add(link.getApplicationName(), link.getNativeIdentity(), AccountRequest.Operation.Disable);
783                Map<String, Object> extraParameters = new HashMap<>();
784                extraParameters.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
785                doProvisioning(user.getName(), plan, doRefresh, extraParameters);
786        }
787
788        /**
789         * Submits a single request to disable all accounts on the Identity that are not already disabled.
790         * @param identity Who to disable the accounts on
791         * @throws GeneralException if any failures occur
792         */
793        public void disableAccounts(Identity identity) throws GeneralException {
794                Objects.requireNonNull(identity, "Provided Identity must not be null");
795                ProvisioningPlan plan = new ProvisioningPlan();
796                plan.setIdentity(identity);
797                for(Link link : Util.safeIterable(identity.getLinks())) {
798                        if (!link.isDisabled()) {
799                                plan.add(link.getApplicationName(), link.getNativeIdentity(), AccountRequest.Operation.Disable);
800                        }
801                }
802                doProvisioning(plan);
803        }
804
805        /**
806         * Submits a single request to disable all accounts on the Identity that are not already disabled.
807         * @param identity Who to disable the accounts on
808         * @param onlyThese Only applications in this list will be disabled
809         * @throws GeneralException if any failures occur
810         */
811        public void disableAccounts(Identity identity, List<String> onlyThese) throws GeneralException {
812                Objects.requireNonNull(identity, "Provided Identity must not be null");
813                ProvisioningPlan plan = new ProvisioningPlan();
814                plan.setIdentity(identity);
815                for(Link link : Util.safeIterable(identity.getLinks())) {
816                        if (!link.isDisabled() && Util.nullSafeContains(onlyThese, link.getApplication())) {
817                                plan.add(link.getApplicationName(), link.getNativeIdentity(), AccountRequest.Operation.Disable);
818                        }
819                }
820                doProvisioning(plan);
821        }
822
823        /**
824         * Submits a single request to disable all accounts on the Identity that are not already disabled.
825         * @param identity Who to disable the accounts on
826         * @param onlyThese Only Link objects where the Predicate returns true will be disabled
827         * @throws GeneralException if any failures occur
828         */
829        public void disableAccounts(Identity identity, Predicate<Link> onlyThese) throws GeneralException {
830                Objects.requireNonNull(identity, "Provided Identity must not be null");
831                ProvisioningPlan plan = new ProvisioningPlan();
832                plan.setIdentity(identity);
833                for(Link link : Util.safeIterable(identity.getLinks())) {
834                        if (!link.isDisabled() && onlyThese.test(link)) {
835                                plan.add(link.getApplicationName(), link.getNativeIdentity(), AccountRequest.Operation.Disable);
836                        }
837                }
838                doProvisioning(plan);
839        }
840
841        /**
842         * Submits a single request to disable all accounts on the Identity that are not already disabled.
843         * @param identity Who to disable the accounts on
844         * @param onlyThese Only Link objects matching the filter will be disabled. This uses the {@link HybridObjectMatcher}, allowing fields like "application.name" in the filter.
845         * @throws GeneralException if any failures occur
846         */
847        public void disableAccounts(Identity identity, Filter onlyThese) throws GeneralException {
848                Objects.requireNonNull(identity, "Provided Identity must not be null");
849                ProvisioningPlan plan = new ProvisioningPlan();
850                plan.setIdentity(identity);
851                for(Link link : Util.safeIterable(identity.getLinks())) {
852                        HybridObjectMatcher matcher = new HybridObjectMatcher(context, onlyThese);
853                        if (!link.isDisabled() && matcher.matches(link)) {
854                                plan.add(link.getApplicationName(), link.getNativeIdentity(), AccountRequest.Operation.Disable);
855                        }
856                }
857                doProvisioning(plan);
858        }
859
860    /**
861     * Submits a provisioning plan using the configured defaults. This plan must have an Identity attached to it using setIdentity().
862     * @param plan The ProvisioningPlan to execute
863     * @throws GeneralException if any failures occur
864     */
865    public ProvisioningProject doProvisioning(ProvisioningPlan plan) throws GeneralException {
866        return doProvisioning(plan, false, provisioningArguments.getDefaultExtraParameters());
867    }
868
869    /**
870     * Submits a provisioning plan using the configured defaults.
871     * @param identityName If the plan does not already have an Identity configured, this one will be used.
872     * @param plan The provisioning plan.
873         * @return The compiled provisioning project, post-provision
874     * @throws GeneralException if any failures occur
875     */
876    public ProvisioningProject doProvisioning(String identityName, ProvisioningPlan plan) throws GeneralException {
877        return doProvisioning(identityName, plan, false);
878    }
879
880    /**
881     * Submits a provisioning plan using the configured defaults and optionally does a refresh.
882     * @param identityName If the plan does not already have an Identity configured, this one will be used.
883     * @param plan The provisioning plan
884     * @param doRefresh If true, a refresh will be performed by the provisioning handler
885         * @return The compiled provisioning project, post-provision
886     * @throws GeneralException if any IIQ failures occur
887     */
888        public ProvisioningProject doProvisioning(String identityName, ProvisioningPlan plan, boolean doRefresh) throws GeneralException {
889        return doProvisioning(identityName, plan, doRefresh, provisioningArguments.getDefaultExtraParameters());
890    }
891
892        /**
893         * Submits a provisioning plan using the configured defaults and optionally does a refresh. Additionally, extra arguments to the workflow can be provided in a Map.
894     * @param identityName If the plan does not already have an Identity configured, this one will be used.
895     * @param plan The provisioning plan
896     * @param doRefresh If true, a refresh will be performed by the provisioning handler
897         * @param extraParameters A Map containing workflow parameters that will be passed to the provisioning workflow or Provisioner
898         * @return The compiled provisioning project, post-provision
899         * @throws GeneralException if any IIQ failures occur
900         */
901        public ProvisioningProject doProvisioning(String identityName, ProvisioningPlan plan, boolean doRefresh, Map<String, Object> extraParameters) throws GeneralException {
902                if (plan.getIdentity() == null) {
903                        plan.setIdentity(context.getObjectByName(Identity.class, identityName));
904                }
905                return doProvisioning(plan, doRefresh, extraParameters);
906        }
907
908        /**
909         * Submits a provisioning plan using the configured defaults and optionally does a refresh. Additionally, extra arguments to the workflow can be provided in a Map.
910         * @param plan The provisioning plan
911         * @param doRefresh If true, a refresh will be performed by the provisioning handler
912         * @param extraParameters A Map containing workflow parameters that will be passed to the provisioning workflow or Provisioner
913         * @throws GeneralException if any IIQ failures occur
914         */
915        public ProvisioningProject doProvisioning(ProvisioningPlan plan, boolean doRefresh, Map<String, Object> extraParameters) throws GeneralException {
916                Attributes<String, Object> arguments = getArguments(plan);
917                if (planArguments != null) {
918                        arguments.putAll(planArguments);
919                }
920                plan.setArguments(arguments);
921                if (beforeProvisioningConsumer != null) {
922                        beforeProvisioningConsumer.accept(plan);
923                }
924                ProvisioningProject outputProject = null;
925                if (isErrorOnAccountSelection() || isErrorOnManualTask() || isErrorOnProvisioningForms() || isErrorOnNewAccount() || projectDebugger != null) {
926                        PlanCompiler compiler = new PlanCompiler(context);
927                        ProvisioningProject project = compiler.compile(new Attributes<>(extraParameters), plan, new Attributes<>());
928                        if (projectDebugger != null) {
929                                projectDebugger.accept(project);
930                        }
931                        if (isErrorOnManualTask() && project.hasUnmanagedPlan()) {
932                                throw new GeneralException("Provisioning request refused because it would result in a manual task");
933                        }
934                        if (isErrorOnProvisioningForms() && (project.hasQuestions() || project.hasUnansweredAccountSelections() || project.hasUnansweredProvisioningTargets())) {
935                                throw new GeneralException("Provisioning request refused because it would result in an unanswered form");
936                        }
937                        if (isErrorOnNewAccount()) {
938                                long count = project.getPlans().stream().flatMap(p -> p.getAccountRequests() != null ? p.getAccountRequests().stream() : null).filter(req -> req.getOperation() != null && req.getOperation().equals(AccountRequest.Operation.Create)).count();
939                                if (count > 0) {
940                                        throw new GeneralException("Provisioning request refused because it would result in a new account creation");
941                                }
942                        }
943                        if (isErrorOnAccountSelection() && (project.hasUnansweredAccountSelections() || project.hasUnansweredProvisioningTargets())) {
944                                throw new GeneralException("Provisioning request refused because we could not identify a target account");
945                        }
946                }
947                if (provisioningArguments.isUseWorkflow()) {
948                String planField = provisioningArguments.getPlanFieldName();
949                String userField = provisioningArguments.getIdentityFieldName();
950
951                Map<String, Object> workflowParameters = new HashMap<>();
952                workflowParameters.put(planField, plan);
953                workflowParameters.put(userField, plan.getIdentity().getName());
954
955                if (doRefresh) {
956                    workflowParameters.put("doRefresh", true);
957                }
958
959                if (extraParameters != null) {
960                                workflowParameters.putAll(extraParameters);
961                }
962
963                Workflow wf = context.getObjectByName(Workflow.class, provisioningArguments.getWorkflowName());
964                Workflower workflower = new Workflower(context);
965                WorkflowLaunch launchOutput = workflower.launch(wf, getCaseName(plan.getIdentity().getName()), workflowParameters);
966
967                if (workflowDebugger != null) {
968                                workflowDebugger.accept(launchOutput);
969                        }
970
971                if (launchOutput != null && launchOutput.isFailed()) {
972                        throw new GeneralException("Workflow launch failed: " + launchOutput.getTaskResult().getMessages());
973                        }
974
975                        if (launchOutput != null && launchOutput.getTaskResult() != null) {
976                                ProvisioningPlan loggingPlan = ProvisioningPlan.getLoggingPlan(plan);
977                                if (loggingPlan != null) {
978                                        launchOutput.getTaskResult().addMessage(Message.info(loggingPlan.toXml()));
979                                }
980
981                                String identityRequest = launchOutput.getTaskResult().getString("identityRequestId");
982                                if (Util.isNotNullOrEmpty(identityRequest)) {
983                                        IdentityRequest ir = context.getObject(IdentityRequest.class, identityRequest);
984                                        if (ir != null) {
985                                                boolean save = false;
986                                                if (Util.isNotNullOrEmpty(plan.getComments())) {
987                                                        ir.addMessage(Message.info(plan.getComments()));
988                                                        save = true;
989                                                }
990                                                if (Util.isNotNullOrEmpty(externalTicketId)) {
991                                                        ir.setExternalTicketId(externalTicketId);
992                                                        save = true;
993                                                }
994                                                if (save) {
995                                                        context.saveObject(ir);
996                                                        context.commitTransaction();
997                                                        context.decache(ir);
998                                                }
999                                                outputProject = ir.getProvisionedProject();
1000                                        }
1001                                }
1002                        }
1003                } else {
1004                        Provisioner provisioner = new Provisioner(context);
1005                        if (extraParameters != null) {
1006                                extraParameters.forEach(provisioner::setArgument);
1007                        }
1008                        provisioner.setDoRefresh(doRefresh);
1009                        provisioner.execute(plan);
1010                        ProvisioningProject project = provisioner.getProject();
1011                        // Do a refresh only if the project is fully committed; otherwise we might break something
1012                        if (project.isFullyCommitted() && doRefresh) {
1013                                Identity id = context.getObjectByName(Identity.class, plan.getIdentity().getName());
1014                                new BaseIdentityUtilities(context).refresh(id, true);
1015                        }
1016                        outputProject = project;
1017                }
1018                return outputProject;
1019    }
1020
1021        /**
1022         * Enables the given account by submitting an Enable provisioning action to IIQ
1023         * @param link The Link to enable
1024         * @throws GeneralException if any IIQ errors occur
1025         */
1026    public void enableAccount(Link link) throws GeneralException {
1027                Objects.requireNonNull(link, "Provided Link must not be null");
1028                ProvisioningPlan plan = new ProvisioningPlan();
1029                String username = ObjectUtil.getIdentityFromLink(context, link.getApplication(), link.getInstance(), link.getNativeIdentity());
1030                Identity user = findIdentity(username);
1031                plan.setIdentity(user);
1032                plan.add(link.getApplicationName(), link.getNativeIdentity(), AccountRequest.Operation.Enable);
1033                doProvisioning(plan);
1034        }
1035
1036        /**
1037     * Finds the identity first by name and then by ID. This is mainly here so that
1038         * it can be overridden by customer-specific subclasses. Otherwise, it does the
1039         * same thing as {@link SailPointContext#getObject(Class, String)}.
1040         *
1041     * @param identityName The identity name to search for
1042     * @return The Identity if found
1043     * @throws GeneralException if any errors occur
1044     */
1045        public Identity findIdentity(String identityName) throws GeneralException {
1046                Objects.requireNonNull(identityName);
1047        Identity output = context.getObjectByName(Identity.class, identityName);
1048        if (output != null) {
1049            return output;
1050        }
1051        output = context.getObjectById(Identity.class, identityName);
1052        return output;
1053    }
1054
1055        /**
1056         * Force retry on the given transaction. There is an out-of-box API for doing this
1057         * on transactions pending retry (to force them to run 'right now' rather than 'later'),
1058         * but there is none for doing this on failed transactions.
1059         *
1060         * @param pt The transaction to retry
1061         * @param createToModify If true, a Create operation will be transmuted to a Modify
1062         * @throws GeneralException if any failures occur
1063         */
1064        public ProvisioningProject forceRetry(ProvisioningTransaction pt, boolean createToModify) throws GeneralException {
1065                Objects.requireNonNull(pt, "Provided ProvisioningTransaction must not be null");
1066                if (pt.getStatus().equals(ProvisioningTransaction.Status.Failed)) {
1067                        AccountRequest request = (AccountRequest)pt.getAttributes().get("request");
1068                        if (request != null) {
1069                                AccountRequest cloned = (AccountRequest)request.cloneRequest();
1070                                ProvisioningResult originalResult = request.getResult();
1071                                if (originalResult != null && originalResult.getStatus() != null) {
1072                                        ProvisioningResult newResult = new ProvisioningResult();
1073                                        newResult.setWarnings(originalResult.getErrors());
1074                                        newResult.setStatus(ProvisioningResult.STATUS_RETRY);
1075                                        cloned.setResult(newResult);
1076                                }
1077                                if (createToModify && cloned.getOperation() != null && cloned.getOperation().equals(AccountRequest.Operation.Create)) {
1078                                        cloned.setOperation(AccountRequest.Operation.Modify);
1079                                }
1080                                Attributes<String, Object> arguments = getArguments(cloned);
1081                                arguments.put("provisioningTransactionId", pt.getId());
1082                                cloned.setArguments(arguments);
1083                                ProvisioningPlan retryPlan = new ProvisioningPlan();
1084                                retryPlan.add(cloned);
1085                                retryPlan.setIdentity(context.getObject(Identity.class, pt.getIdentityName()));
1086                                retryPlan.setTargetIntegration(pt.getIntegration());
1087                                retryPlan.setComments("Retry for Provisioning Transaction ID " + pt.getId());
1088                                setUseWorkflow(false);
1089                                return doProvisioning(retryPlan.getIdentity().getName(), retryPlan, false, new HashMap<>());
1090                        } else {
1091                                throw new IllegalArgumentException("You can only forceRetry on transaction with an AccountRequest attached");
1092                        }
1093                } else {
1094                        throw new IllegalArgumentException("You can only forceRetry on failed transactions; IIQ will redo 'retry' transactions on its own");
1095                }
1096        }
1097
1098        /**
1099         * Builds the case name based on the template provided
1100         * @param identityName The identity name passed to this case
1101         * @return The case name generated
1102         */
1103        public String getCaseName(String identityName) {
1104                String caseName = provisioningArguments.getCaseNameTemplate().replace("{Workflow}", provisioningArguments.getWorkflowName());
1105                caseName = caseName.replace("{IdentityName}", identityName);
1106                caseName = caseName.replace("{Timestamp}", String.valueOf(System.currentTimeMillis()));
1107                if (caseName.contains("{Date}")) {
1108                        DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT;
1109                        Instant now = Instant.now();
1110                        caseName = caseName.replace("{Date}", formatter.format(now));
1111                }
1112
1113                return caseName;
1114        }
1115
1116        public String getCaseNameTemplate() {
1117                return provisioningArguments.getCaseNameTemplate();
1118        }
1119        
1120        public String getExternalTicketId() {
1121                return externalTicketId;
1122        }
1123
1124        public String getProvisioningWorkflow() {
1125                return provisioningArguments.getWorkflowName();
1126        }
1127
1128        public boolean isErrorOnAccountSelection() {
1129                return provisioningArguments.isErrorOnAccountSelection();
1130        }
1131
1132        public boolean isErrorOnManualTask() {
1133                return provisioningArguments.isErrorOnManualTask();
1134        }
1135
1136        public boolean isErrorOnNewAccount() {
1137                return provisioningArguments.isErrorOnNewAccount();
1138        }
1139
1140        public boolean isErrorOnProvisioningForms() {
1141                return provisioningArguments.isErrorOnProvisioningForms();
1142        }
1143
1144        public boolean isUseWorkflow() {
1145                return provisioningArguments.isUseWorkflow();
1146        }
1147
1148        /**
1149         * Moves the given target account(s) to the given target owner
1150         * @param targetOwner The target owner for the given accounts
1151         * @param accounts One or more accounts to move to the new owner
1152         * @throws GeneralException if anything goes wrong during provisioning
1153         */
1154        public void moveLinks(Identity targetOwner, Link... accounts) throws GeneralException {
1155                if (accounts != null && accounts.length > 0) {
1156                        ProvisioningPlan plan = new ProvisioningPlan();
1157                        for (Link account : accounts) {
1158                                linkMovePlan(account, targetOwner, plan);
1159                        }
1160                        doProvisioning(plan);
1161                }
1162        }
1163
1164        /**
1165         * Removes all entitlements and assigned roles from the given Identity
1166         *
1167         * @param identity The identity from whom to strip access
1168         * @throws GeneralException if anything goes wrong during provisioning
1169         */
1170        public void removeAllAccess(Identity identity) throws GeneralException {
1171                ProvisioningPlan entitlementPlan = new ProvisioningPlan();
1172                entitlementPlan.setIdentity(identity);
1173
1174                for(Link account : Util.safeIterable(identity.getLinks())) {
1175                        List<String> entitlementAttributes = account.getApplication().getEntitlementAttributeNames();
1176                        for(String attribute : Util.safeIterable(entitlementAttributes)) {
1177                                removeAllEntitlements(account, attribute, entitlementPlan);
1178                        }
1179                }
1180
1181                Map<String, Object> options = new HashMap<>();
1182                options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1183
1184                doProvisioning(identity.getName(), entitlementPlan, false, options);
1185
1186                ObjectUtil.saveDecacheAttach(context, identity);
1187
1188                ProvisioningPlan rolePlan = new ProvisioningPlan();
1189                rolePlan.setIdentity(identity);
1190
1191                for(Bundle role : Util.safeIterable(identity.getAssignedRoles())) {
1192                        removeUserRolePlan(role.getName(), true, rolePlan);
1193                }
1194
1195                doProvisioning(identity.getName(), rolePlan, false, options);
1196
1197                ObjectUtil.saveDecacheAttach(context, identity);
1198        }
1199
1200        /**
1201         * Removes all entitlements from all accounts of the given type on the given user
1202         * @param identity The identity to target
1203         * @param target The target application from which to remove accounts
1204         * @throws GeneralException if a failure occurs
1205         */
1206    public void removeAllEntitlements(Identity identity, Application target) throws GeneralException {
1207        ProvisioningPlan plan = new ProvisioningPlan();
1208        plan.setIdentity(identity);
1209                removeAllEntitlements(identity, target, plan);
1210                Map<String, Object> options = new HashMap<>();
1211                options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1212
1213                doProvisioning(identity.getName(), plan, false, options);
1214        }
1215
1216        /**
1217         * Modifies the plan to add entitlement removal requests for all entitlements on
1218         * accounts of the given type
1219         * @param identity The identity from which to extract the entitlements
1220         * @param target The target application
1221         * @param plan The provisioning plan
1222         * @throws GeneralException if any failures occur
1223         */
1224        public void removeAllEntitlements(Identity identity, Application target, ProvisioningPlan plan) throws GeneralException {
1225                IdentityService identityService = new IdentityService(context);
1226                List<Link> links = identityService.getLinks(identity, target);
1227                List<String> entitlementAttributes = target.getEntitlementAttributeNames();
1228                for(Link account : Util.safeIterable(links)) {
1229                        for(String attribute : Util.safeIterable(entitlementAttributes)) {
1230                                removeAllEntitlements(account, attribute, plan);
1231                        }
1232                }
1233        }
1234
1235        /**
1236         * Modifies the plan to remove all values from the given attribute on the given
1237         * account.
1238         * @param account The account to modify
1239         * @param attribute The attribute to remove attributes from
1240         * @param plan The provisioning plan
1241         */
1242        @SuppressWarnings("unchecked")
1243        public void removeAllEntitlements(Link account, String attribute, ProvisioningPlan plan) {
1244                if (account == null || account.getAttributes() == null) {
1245                        return;
1246                }
1247                List<String> values = account.getAttributes().getList(attribute);
1248                if (values != null && !values.isEmpty()) {
1249                        AccountRequest accountRequest = plan.getAccountRequest(account.getApplicationName(), null, account.getNativeIdentity());
1250                        if (accountRequest == null) {
1251                                accountRequest = new AccountRequest(AccountRequest.Operation.Modify, account.getApplicationName(), null, account.getNativeIdentity());
1252                                plan.add(accountRequest);
1253                        }
1254                        AttributeRequest attributeRequest = new AttributeRequest(attribute, ProvisioningPlan.Operation.Remove, values);
1255                        attributeRequest.setAssignment(true);
1256                        accountRequest.add(attributeRequest);
1257                }
1258        }
1259
1260        /**
1261         * Removes the given entitlement from the given account on the user
1262         * @param identityName The identity name
1263         * @param account The account to modify
1264         * @param entitlement The managed attribute from which to extract the entitlement
1265         * @param withApproval If false, approval will be skipped
1266         * @throws GeneralException if any failure occurs
1267         */
1268        public void removeEntitlement(String identityName, Link account, ManagedAttribute entitlement, boolean withApproval) throws GeneralException {
1269                ProvisioningPlan plan = new ProvisioningPlan();
1270                Identity identity = findIdentity(identityName);
1271                plan.setIdentity(identity);
1272
1273                AccountRequest accountRequest = new AccountRequest(AccountRequest.Operation.Modify, account.getApplicationName(), account.getInstance(), account.getNativeIdentity());
1274                plan.add(accountRequest);
1275
1276                AttributeRequest attributeRequest = new AttributeRequest(entitlement.getName(), ProvisioningPlan.Operation.Remove, entitlement.getValue());
1277                attributeRequest.setAssignment(true);
1278                accountRequest.add(attributeRequest);
1279
1280                Map<String, Object> options = new HashMap<>();
1281                if (!withApproval) {
1282                        options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1283                }
1284
1285                doProvisioning(identityName, plan, false, options);
1286        }
1287        
1288        /**
1289         * Removes the given user from the given role
1290         * @param targetAssignment The target existing RoleAssignment from an Identity
1291         * @throws GeneralException if a provisioning failure occurs
1292         */
1293        public void removeUserRole(String identityName, RoleAssignment targetAssignment) throws GeneralException {
1294                Objects.requireNonNull(identityName, "A non-null Identity name is required");
1295                Objects.requireNonNull(targetAssignment, "The provided RoleAssignment must not be null");
1296                if (targetAssignment.isNegative()) {
1297                        throw new IllegalArgumentException("Cannot remove a negative assignment using this API");
1298                }
1299                ProvisioningPlan.Operation roleOp = ProvisioningPlan.Operation.Remove;
1300                if (Util.nullSafeEq(targetAssignment.getSource(), "Rule")) {
1301                        roleOp = ProvisioningPlan.Operation.Revoke;
1302                }
1303                Identity identity = findIdentity(identityName);
1304                ProvisioningPlan provisioningPlan = new ProvisioningPlan();
1305                provisioningPlan.setIdentity(identity);
1306                AccountRequest accountRequest = new AccountRequest(AccountRequest.Operation.Modify, ProvisioningPlan.APP_IIQ, null, identity.getName());
1307                AttributeRequest attributeRequest = new AttributeRequest(ASSIGNED_ROLES_ATTR, roleOp, targetAssignment.getRoleName());
1308                attributeRequest.setAssignmentId(targetAssignment.getAssignmentId());
1309                accountRequest.add(attributeRequest);
1310                provisioningPlan.add(accountRequest);
1311                Map<String, Object> options = new HashMap<>();
1312                options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1313                doProvisioning(identity.getName(), provisioningPlan, false, options);
1314        }
1315
1316        /**
1317         * Removes the given user from the given role. For a detected role, this will
1318         * remove any entitlements provisioned by that role that are not required by
1319         * another role assigned to the user.
1320         *
1321         * @param identityName The name of the Identity to modify
1322         * @param targetDetection The target existing RoleDetection from an Identity
1323         * @throws GeneralException if a provisioning failure occurs
1324         */
1325        public void removeUserRole(String identityName, RoleDetection targetDetection) throws GeneralException {
1326                Objects.requireNonNull(identityName, "A non-null Identity name is required");
1327                Objects.requireNonNull(targetDetection, "The provided RoleDetection must not be null");
1328                if (targetDetection.hasAssignmentIds()) {
1329                        throw new IllegalArgumentException("Cannot remove a required detected role using this API");
1330                }
1331                String tempAssignmentKey = "TEMP:" + Util.uuid();
1332                Identity identity = findIdentity(identityName);
1333                ProvisioningPlan provisioningPlan = new ProvisioningPlan();
1334                provisioningPlan.setIdentity(identity);
1335                AccountRequest accountRequest = new AccountRequest(AccountRequest.Operation.Modify, ProvisioningPlan.APP_IIQ, null, identity.getName());
1336                AttributeRequest attributeRequest = new AttributeRequest("detectedRoles", ProvisioningPlan.Operation.Remove, targetDetection.getRoleName());
1337
1338                BaseIdentityUtilities identityUtilities = new BaseIdentityUtilities(context);
1339                if (identityUtilities.hasMultiple(identity, targetDetection.getRoleName())) {
1340                        ProvisioningTarget target = new ProvisioningTarget();
1341                        target.setAssignmentId(tempAssignmentKey);
1342                        target.setRole(targetDetection.getRoleName());
1343                        Utilities.safeStream(targetDetection.getTargets()).map(ProvisioningUtilities::roleTargetToAccountSelection).forEach(target::addAccountSelection);
1344                        if (Util.nullSafeSize(target.getAccountSelections()) > 0) {
1345                                List<ProvisioningTarget> targets = provisioningPlan.getProvisioningTargets();
1346                                if (targets == null) {
1347                                        targets = new ArrayList<>();
1348                                }
1349                                targets.add(target);
1350                                provisioningPlan.setProvisioningTargets(targets);
1351                                attributeRequest.setAssignmentId(tempAssignmentKey);
1352                        }
1353                }
1354                accountRequest.add(attributeRequest);
1355                provisioningPlan.add(accountRequest);
1356                Map<String, Object> options = new HashMap<>();
1357                options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1358                doProvisioning(identity.getName(), provisioningPlan, false, options);
1359        }
1360        
1361        /**
1362         * Removes the given user from the given role associated with the target provisioned account.
1363         * Note that the role may also be associated with a different account. This is used only to
1364         * locate the RoleAssignment object for deprovisioning by assignment ID.
1365         * @param identityName The identity name to add to the given role
1366         * @param roleName The role to add
1367         * @param withApproval If true, default approval will be required
1368         * @param target If not null, this will be used as a provisioning target for the plan
1369         * @throws GeneralException if a provisioning failure occurs
1370         */
1371        public void removeUserRole(String identityName, String roleName, boolean withApproval, Link target) throws GeneralException {
1372                // We have to generate our own assignment ID in this case
1373                Bundle role = context.getObjectByName(Bundle.class, roleName);
1374                if (role == null) {
1375                        throw new IllegalArgumentException("Role " + roleName + " does not exist");
1376                }
1377                Identity identity = findIdentity(identityName);
1378                List<RoleAssignment> existingAssignments = identity.getRoleAssignments(role);
1379                RoleAssignment targetAssignment = null;
1380                for(RoleAssignment ra : existingAssignments) {
1381                        RoleTarget rt = new RoleTarget(target);
1382                        if (ra.hasMatchingRoleTarget(rt)) {
1383                                targetAssignment = ra;
1384                                break;
1385                        }
1386                }
1387                if (targetAssignment == null) {
1388                        throw new IllegalArgumentException("The user " + identityName + " does not have a role " + roleName + " targeting " + target.getApplicationName() + " account " + target.getNativeIdentity());
1389                }
1390                ProvisioningPlan provisioningPlan = new ProvisioningPlan();
1391                provisioningPlan.setIdentity(identity);
1392                AccountRequest accountRequest = new AccountRequest(AccountRequest.Operation.Modify, ProvisioningPlan.APP_IIQ, null, identity.getName());
1393                AttributeRequest attributeRequest = new AttributeRequest(ASSIGNED_ROLES_ATTR, ProvisioningPlan.Operation.Remove, roleName);
1394                attributeRequest.setAssignmentId(targetAssignment.getAssignmentId());
1395                accountRequest.add(attributeRequest);
1396                provisioningPlan.add(accountRequest);
1397                Map<String, Object> options = new HashMap<>();
1398                if (!withApproval) {
1399                        options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1400                }
1401                doProvisioning(provisioningPlan.getIdentity().getName(), provisioningPlan, false, options);
1402        }
1403
1404        /**
1405         * Removes the given role from the given user
1406         * @param identityName The identity to remove the role from
1407         * @param roleName The role to remove from the identity
1408         * @throws GeneralException If a failure occurs
1409         */
1410        public void removeUserRole(String identityName, String roleName) throws GeneralException {
1411                removeUserRole(identityName, roleName, false, false);
1412        }
1413
1414        /**
1415         * Removes the given role from the given user
1416         * @param identityName The identity to remove the role from
1417         * @param roleName The role to remove from the identity
1418         * @param withApproval If true, a default approval will be required
1419         * @throws GeneralException If a failure occurs
1420         */
1421        public void removeUserRole(String identityName, String roleName, boolean withApproval) throws GeneralException {
1422                removeUserRole(identityName, roleName, withApproval, false);
1423        }
1424
1425        /**
1426         * Removes the given role from the given user
1427         * @param identityName The identity to remove the role from
1428         * @param roleName The role to remove from the identity
1429         * @param withApproval If true, a default approval will be required
1430         * @param revoke If true, the role will be revoked and not removed
1431         * @throws GeneralException If a failure occurs
1432         */
1433    public void removeUserRole(String identityName, String roleName, boolean withApproval, boolean revoke) throws GeneralException {
1434        ProvisioningPlan provisioningPlan = new ProvisioningPlan();
1435        Identity identity = findIdentity(identityName);
1436        provisioningPlan.setIdentity(identity);
1437                removeUserRolePlan(roleName, revoke, provisioningPlan);
1438        Map<String, Object> options = new HashMap<>();
1439        if (!withApproval) {
1440            options.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1441        }
1442        doProvisioning(identity.getName(), provisioningPlan, false, options);
1443    }
1444
1445        public void setBeforeProvisioning(Consumer<ProvisioningPlan> planConsumer) {
1446                this.beforeProvisioningConsumer = planConsumer;
1447        }
1448
1449        public void setCaseNameTemplate(String caseNameTemplate) {
1450                provisioningArguments.setCaseNameTemplate(caseNameTemplate);
1451        }
1452
1453        public void setErrorOnAccountSelection(boolean errorOnAccountSelection) {
1454                provisioningArguments.setErrorOnAccountSelection(errorOnAccountSelection);
1455        }
1456
1457        public void setErrorOnManualTask(boolean errorOnManualTask) {
1458                provisioningArguments.setErrorOnManualTask(errorOnManualTask);
1459        }
1460
1461        public void setErrorOnNewAccount(boolean errorOnNewAccount) {
1462                provisioningArguments.setErrorOnNewAccount(errorOnNewAccount);
1463        }
1464
1465        public void setErrorOnProvisioningForms(boolean errorOnProvisioningForms) {
1466                provisioningArguments.setErrorOnProvisioningForms(errorOnProvisioningForms);
1467        }
1468
1469        public void setExternalTicketId(String externalTicketId) {
1470                this.externalTicketId = externalTicketId;
1471        }
1472        
1473        public void setProjectDebugger(Consumer<ProvisioningProject> projectDebugger) {
1474                this.projectDebugger = projectDebugger;
1475        }
1476
1477        public void setLauncher(String who) {
1478                addWorkflowArgument("launcher", who);
1479        }
1480
1481        /**
1482         * Sets workflow configuration items all at once for this utility
1483         * @param config The workflow configuration to use
1484         */
1485        public void setProvisioningArguments(ProvisioningArguments config) {
1486                this.provisioningArguments.merge(config);
1487        }
1488        
1489        public void setProvisioningWorkflow(String provisioningWorkflow) {
1490                provisioningArguments.setWorkflowName(provisioningWorkflow);
1491        }
1492
1493        public void setUseWorkflow(boolean useWorkflow) {
1494                provisioningArguments.setUseWorkflow(useWorkflow);
1495        }
1496
1497        public void setWorkflowDebugger(Consumer<WorkflowLaunch> workflowDebugger) {
1498                this.workflowDebugger = workflowDebugger;
1499        }
1500
1501        /**
1502         * Transforms this object into a Map that can be passed to the constructor
1503         * that takes a Map
1504         *
1505         * @return The resulting map transformation
1506         */
1507        public Attributes<String, Object> toMap() {
1508                Attributes<String, Object> map = new Attributes<>();
1509                map.put("caseNameTemplate", this.provisioningArguments.getCaseNameTemplate());
1510                map.put("defaultExtraParameters", this.provisioningArguments.getDefaultExtraParameters());
1511                map.put("errorOnAccountSelection", this.provisioningArguments.isErrorOnAccountSelection());
1512                map.put("errorOnManualTask", this.provisioningArguments.isErrorOnManualTask());
1513                map.put("errorOnNewAccount", this.provisioningArguments.isErrorOnNewAccount());
1514                map.put("errorOnProvisioningForms", this.provisioningArguments.isErrorOnProvisioningForms());
1515                map.put("externalTicketId", this.externalTicketId);
1516                map.put("planArguments", this.planArguments);
1517                map.put("workflowPlanField", this.provisioningArguments.getPlanFieldName());
1518                map.put("workflowIdentityField", this.provisioningArguments.getIdentityFieldName());
1519                map.put("provisioningWorkflow", this.provisioningArguments.getWorkflowName());
1520                map.put("useWorkflow", this.provisioningArguments.isUseWorkflow());
1521                return map;
1522        }
1523
1524        /**
1525         * Updates the given link with the given values. Field names can also have the form "Operation:Name", e.g. "Add:memberOf", to specify an operation.
1526         *
1527         * Values 'Set' to a multi-value field will be transformed to 'Add' by default. You can override this using the colon syntax above, which will always take priority.
1528         *
1529         * @param link The Link to update
1530         * @param map The values to update (Set by default)
1531         * @throws GeneralException if any provisioning failures occur
1532         */
1533        public void updateAccount(Link link, Map<String, Object> map) throws GeneralException {
1534                Objects.requireNonNull(link, "The provided Link must not be null");
1535                if (map == null || map.isEmpty()) {
1536                        log.warn("Call made to updateAccount() with a null or empty map of updates");
1537                        return;
1538                }
1539                ProvisioningPlan plan = new ProvisioningPlan();
1540                String username = ObjectUtil.getIdentityFromLink(context, link.getApplication(), link.getInstance(), link.getNativeIdentity());
1541                Identity user = findIdentity(username);
1542
1543                plan.setIdentity(user);
1544
1545                for(String key : map.keySet()) {
1546                        String provisioningName = key;
1547                        ProvisioningPlan.Operation operation = ProvisioningPlan.Operation.Set;
1548                        if (key.contains(":")) {
1549                                String[] components = key.split(":");
1550                                operation = ProvisioningPlan.Operation.valueOf(components[0]);
1551                                provisioningName = components[1];
1552                        }
1553                        // For multi-valued attributes, transform set to add by default
1554                        if (operation.equals(ProvisioningPlan.Operation.Set) && !key.contains(":")) {
1555                                AttributeDefinition attributeDefinition = link.getApplication().getAccountSchema().getAttributeDefinition(provisioningName);
1556                                if (attributeDefinition.isMultiValued()) {
1557                                        operation = ProvisioningPlan.Operation.Add;
1558                                }
1559                        }
1560                        AccountRequest request = plan.add(link.getApplicationName(), link.getNativeIdentity(), provisioningName, operation, map.get(key));
1561                        if (request.getOperation() == null) {
1562                                request.setOperation(AccountRequest.Operation.Modify);
1563                        }
1564                }
1565
1566                Map<String, Object> extraParameters = new HashMap<>();
1567                extraParameters.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1568                doProvisioning(username, plan, false, extraParameters);
1569        }
1570
1571        /**
1572         * Updates the given link by setting or adding the given values. Multi-value attributes will be transformed to Set.
1573         *
1574         * @param link The Link to update
1575         * @param attribute The name of the attribute to either set or add
1576         * @param value The value(s) to set or add
1577         * @throws GeneralException if any provisioning failures occur
1578         */
1579        public void updateAccountRemove(Link link, String attribute, Object value) throws GeneralException {
1580                Objects.requireNonNull(link, "The provided Link must not be null");
1581                ProvisioningPlan plan = new ProvisioningPlan();
1582                String username = ObjectUtil.getIdentityFromLink(context, link.getApplication(), link.getInstance(), link.getNativeIdentity());
1583                Identity user = findIdentity(username);
1584
1585                plan.setIdentity(user);
1586
1587                AccountRequest request = plan.add(link.getApplicationName(), link.getNativeIdentity(), attribute, ProvisioningPlan.Operation.Remove, value);
1588                if (request.getOperation() == null) {
1589                        request.setOperation(AccountRequest.Operation.Modify);
1590                }
1591
1592                Map<String, Object> extraParameters = new HashMap<>();
1593                extraParameters.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1594                doProvisioning(username, plan, false, extraParameters);
1595        }
1596
1597        /**
1598         * Updates the given link by setting or adding the given values. Multi-value attributes will be transformed to Set.
1599         *
1600         * @param link The Link to update
1601         * @param attribute The name of the attribute to either set or add
1602         * @param value The value(s) to set or add
1603         * @throws GeneralException if any provisioning failures occur
1604         */
1605        public void updateAccountSet(Link link, String attribute, Object value) throws GeneralException {
1606                Objects.requireNonNull(link, "The provided Link must not be null");
1607                ProvisioningPlan plan = new ProvisioningPlan();
1608                String username = ObjectUtil.getIdentityFromLink(context, link.getApplication(), link.getInstance(), link.getNativeIdentity());
1609                Identity user = findIdentity(username);
1610
1611                plan.setIdentity(user);
1612
1613                ProvisioningPlan.Operation operation = ProvisioningPlan.Operation.Set;
1614                // For multi-valued attributes, transform set to add
1615                AttributeDefinition attributeDefinition = link.getApplication().getAccountSchema().getAttributeDefinition(attribute);
1616                if (attributeDefinition != null && attributeDefinition.isMultiValued()) {
1617                        operation = ProvisioningPlan.Operation.Add;
1618                }
1619                AccountRequest request = plan.add(link.getApplicationName(), link.getNativeIdentity(), attribute, operation, value);
1620                if (request.getOperation() == null) {
1621                        request.setOperation(AccountRequest.Operation.Modify);
1622                }
1623                Map<String, Object> extraParameters = new HashMap<>();
1624                extraParameters.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1625                doProvisioning(username, plan, false, extraParameters);
1626        }
1627
1628        /**
1629         * Updates the given identity with the given values. Field names can also have the form "Operation:Name", e.g. "Add:memberOf", to specify an operation.
1630         *
1631         * Values 'Set' to a multi-value field will be transformed to 'Add' by default. You can override this using the colon syntax above, which will always take priority.
1632         *
1633         * @param identity The identity to modify
1634         * @param params The parameters to modify
1635         * @throws GeneralException if anything goes wrong
1636         */
1637        public void updateUser(Identity identity, Map<String, Object> params) throws GeneralException {
1638                updateUser(identity, "Set", params);
1639        }
1640
1641        /**
1642         * Updates the given identity with the given values. Field names can also have the form "Operation:Name", e.g. "Add:memberOf", to specify an operation.
1643         *
1644         * Values 'Set' to a multi-value field will be transformed to 'Add' by default. You can override this using the colon syntax above, which will always take priority.
1645         *
1646         * @param identity The identity to modify
1647         * @param defaultOperation The default operation to update with (Set, Add, Remove, etc) if one is not given
1648         * @param params The parameters to modify
1649         * @throws GeneralException if anything goes wrong
1650         */
1651        public void updateUser(Identity identity, String defaultOperation, Map<String, Object> params) throws GeneralException {
1652                if (params == null || params.isEmpty()) {
1653                        log.warn("Call made to updateAccount() with a null or empty map of updates");
1654                        return;
1655                }
1656                Objects.requireNonNull(identity, "Identity must not be null");
1657                ProvisioningPlan plan = new ProvisioningPlan();
1658                plan.setIdentity(identity);
1659                for(String key : params.keySet()) {
1660                        String provisioningName = key;
1661                        ProvisioningPlan.Operation operation = ProvisioningPlan.Operation.valueOf(defaultOperation);
1662                        if (key.contains(":")) {
1663                                String[] components = key.split(":");
1664                                operation = ProvisioningPlan.Operation.valueOf(components[0]);
1665                                provisioningName = components[1];
1666                        }
1667                        // For multi-valued attributes, transform set to add by default
1668                        if (operation.equals(ProvisioningPlan.Operation.Set) && !key.contains(":")) {
1669                                ObjectConfig identityConfig = Identity.getObjectConfig();
1670                                ObjectAttribute attributeDefinition = identityConfig.getObjectAttribute(provisioningName);
1671                                if (attributeDefinition.isMultiValued()) {
1672                                        operation = ProvisioningPlan.Operation.Add;
1673                                }
1674                        }
1675                        Object value = params.get(key);
1676                        if (value instanceof Identity) {
1677                                value = ((Identity) value).getName();
1678                        }
1679                        plan.add(ProvisioningPlan.APP_IIQ, identity.getName(), provisioningName, operation, value);
1680                }
1681                Map<String, Object> extraParameters = new HashMap<>();
1682                extraParameters.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1683                doProvisioning(identity.getName(), plan, false, extraParameters);
1684        }
1685
1686        /**
1687         * Updates the given user with the given field values
1688         * @param identity The identity in question
1689         * @param field The field to set
1690         * @param operation The operation to use
1691         * @param value The value to update
1692         * @throws GeneralException if any provisioning failures occur
1693         */
1694        public void updateUser(Identity identity, String field, ProvisioningPlan.Operation operation, Object value) throws GeneralException {
1695                ProvisioningPlan changes = new ProvisioningPlan();
1696                if (value instanceof Identity) {
1697                        value = ((Identity) value).getName();
1698                }
1699                changes.add(ProvisioningPlan.APP_IIQ, identity.getName(), field, operation, value);
1700                changes.setIdentity(identity);
1701                Map<String, Object> extraParameters = new HashMap<>();
1702                extraParameters.put(PLAN_PARAM_APPROVAL_SCHEME, NO_APPROVAL_SCHEME);
1703                doProvisioning(identity.getName(), changes, false, extraParameters);
1704        }
1705
1706}