001package com.identityworksllc.iiq.common;
002
003import sailpoint.api.SailPointContext;
004import sailpoint.object.*;
005import sailpoint.search.MapMatcher;
006import sailpoint.tools.GeneralException;
007import sailpoint.tools.Pair;
008import sailpoint.tools.Util;
009
010import java.util.*;
011import java.util.function.BiPredicate;
012import java.util.function.Predicate;
013import java.util.stream.Collectors;
014
015/**
016 * Utilities for acting on and generating ProvisioningPlan objects
017 */
018@SuppressWarnings("unused")
019public class Plans {
020
021    /**
022     * A value you can pass to either removeAttributeRequest or hasAttributeRequest
023     * to indicate a literal null instead of a wildcard (which is the default interpretation
024     * of null inputs to those methods).
025     *
026     * This will match anything that {@link Utilities#isNothing(Object)} matches.
027     */
028    public static final String EMPTY_VALUE = Plans.class.getName() + "*NULL*";
029
030    /**
031     * Adds an AccountRequest to the given plan for each non-disabled account owned by the user
032     * unless the account is matched by the exceptFilter.
033     */
034    public static void disableAccounts(SailPointContext context, Identity target, ProvisioningPlan plan, Filter exceptFilter) throws GeneralException {
035        MapMatcher matcher = null;
036        if (exceptFilter != null) {
037            matcher = new MapMatcher(exceptFilter);
038        }
039        for(Link link : Util.safeIterable(target.getLinks())) {
040            if (!link.isDisabled() && (matcher == null || !matcher.matches(Mapper.toMap(link)))) {
041                plan.add(link.getApplicationName(), link.getNativeIdentity(), ProvisioningPlan.AccountRequest.Operation.Disable);
042            }
043        }
044    }
045
046    /**
047     * Empties the input plan's account and object requests in place (i.e, by modifying
048     * the Lists within the plan itself). This can be used to cancel a provisioning operation
049     * in a Before Provisioning rule, for example.
050     *
051     * @param plan The plan
052     */
053    public static void emptyPlan(ProvisioningPlan plan) {
054        if (plan != null) {
055            if (plan.getAccountRequests() != null) {
056                plan.getAccountRequests().clear();
057            }
058            if (plan.getObjectRequests() != null) {
059                plan.getObjectRequests().clear();
060            }
061        }
062    }
063
064    /**
065     * Adds an AccountRequest to the given plan for each non-disabled account owned by the user
066     * unless the account is matched by the exceptFilter.
067     */
068    public static void enableAccounts(SailPointContext context, Identity target, ProvisioningPlan plan, Filter exceptFilter) throws GeneralException {
069        MapMatcher matcher = null;
070        if (exceptFilter != null) {
071            matcher = new MapMatcher(exceptFilter);
072        }
073        for(Link link : Util.safeIterable(target.getLinks())) {
074            if (link.isDisabled() && (matcher == null || !matcher.matches(Mapper.toMap(link)))) {
075                plan.add(link.getApplicationName(), link.getNativeIdentity(), ProvisioningPlan.AccountRequest.Operation.Enable);
076            }
077        }
078    }
079
080    /**
081     * If an entitlement is being added to the given application in the given plan, also
082     * add a separate Enable operation. This is important for connectors like Salesforce
083     * where entitlements cannot be added to disabled accounts.
084     */
085    public static void enableOnEntitlementAdd(ProvisioningPlan plan, Application application, boolean enableFirst) {
086        Schema appSchema = application.getAccountSchema();
087        boolean expandEnable = false;
088        ProvisioningPlan.AccountRequest template = null;
089        for(ProvisioningPlan.AccountRequest accountRequest : Util.safeIterable(plan.getAccountRequests(application.getName()))) {
090            for(ProvisioningPlan.AttributeRequest attributeRequest : Util.safeIterable(accountRequest.getAttributeRequests())) {
091                if (attributeRequest.getOperation().equals(ProvisioningPlan.Operation.Add) || attributeRequest.getOperation().equals(ProvisioningPlan.Operation.Set)) {
092                    String attributeName = attributeRequest.getName();
093                    AttributeDefinition attributeDefinition = appSchema.getAttributeDefinition(attributeName);
094                    if (attributeDefinition != null) {
095                        if (attributeDefinition.isEntitlement() || attributeDefinition.isManaged()) {
096                            expandEnable = true;
097                            template = accountRequest;
098                            break;
099                        }
100                    }
101                }
102            }
103        }
104        if (expandEnable) {
105            ProvisioningPlan.AccountRequest enable = new ProvisioningPlan.AccountRequest();
106            enable.setOperation(ProvisioningPlan.AccountRequest.Operation.Enable);
107            enable.setNativeIdentity(template.getNativeIdentity());
108            enable.setApplication(template.getApplicationName());
109            enable.setInstance(template.getInstance());
110            enable.setComments("Expand entitlement add to enable");
111            enable.setArguments(template.getArguments());
112            List<ProvisioningPlan.AccountRequest> requests = plan.getAccountRequests();
113            if (enableFirst) {
114                requests.add(0, enable);
115            } else {
116                requests.add(enable);
117            }
118            plan.setAccountRequests(requests);
119        }
120    }
121
122    /**
123     * Extracts the target attribute from its AccountRequest into a new, second request
124     * against the same account.
125     */
126    public static void extractToNewRequest(ProvisioningPlan plan, String targetAttribute) {
127        extractToNewRequest(plan, targetAttribute, false);
128    }
129
130    /**
131     * Extracts the target attribute from its AccountRequest into a new, second request
132     * against the same account. The new request will be placed at the beginning of the
133     * ProvisioningPlan sequence.
134     */
135    public static void extractToNewRequest(ProvisioningPlan plan, String targetAttribute, boolean atBeginning) {
136        if (plan.getAccountRequests() == null) {
137            return;
138        }
139        List<ProvisioningPlan.AccountRequest> toAdd = new ArrayList<>();
140        for(ProvisioningPlan.AccountRequest accountRequest : plan.getAccountRequests()) {
141            if (accountRequest.getAttributeRequests() == null) {
142                continue;
143            }
144            if (accountRequest.getAttributeRequest(targetAttribute) != null) {
145                ProvisioningPlan.AttributeRequest attribute = accountRequest.getAttributeRequest(targetAttribute);
146                ProvisioningPlan.AccountRequest newRequest = accountRequest.clone();
147                newRequest.cloneAccountProperties(accountRequest);
148                newRequest.setAttributeRequests(new ArrayList<>());
149                newRequest.add(attribute);
150                accountRequest.remove(attribute);
151                toAdd.add(newRequest);
152            }
153        }
154        List<ProvisioningPlan.AccountRequest> requests = plan.getAccountRequests();
155        if (atBeginning) {
156            requests.addAll(0, toAdd);
157        } else {
158            requests.addAll(toAdd);
159        }
160        plan.setAccountRequests(requests);
161    }
162
163    /**
164     * Finds attribute requests in the provisioning plan where the account matches the filter
165     *
166     * @param accountRequest The provisioning plan
167     * @param findPredicate The account filter
168     * @return The list of account requests
169     */
170    public static List<ProvisioningPlan.AttributeRequest> find(ProvisioningPlan.AccountRequest accountRequest, Predicate<ProvisioningPlan.AttributeRequest> findPredicate) {
171        return Utilities.safeStream(accountRequest.getAttributeRequests()).filter(findPredicate).collect(Collectors.toList());
172    }
173
174    /**
175     * Finds attribute requests in the provisioning plan where the account matches the filter
176     *
177     * @param plan The provisioning plan
178     * @param findPredicate The account filter
179     * @return The list of account requests
180     */
181    public static List<ProvisioningPlan.AccountRequest> find(ProvisioningPlan plan, Predicate<ProvisioningPlan.AccountRequest> findPredicate) {
182        return Utilities.safeStream(plan.getAccountRequests()).filter(findPredicate).collect(Collectors.toList());
183    }
184
185    /**
186     * Find pairs of account/attribute requests in the provisioning plan where the
187     * account and attribute together match the filter.
188     *
189     * @param plan The provisioning plan
190     * @param findPredicate The account and attribute filter
191     * @return The list of account/attribute pairs
192     */
193    public static List<Pair<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest>> find(ProvisioningPlan plan, BiPredicate<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest> findPredicate) {
194        List<Pair<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest>> pairs = new ArrayList<>();
195        for(ProvisioningPlan.AccountRequest accountRequest : Util.safeIterable(plan.getAccountRequests())) {
196            for(ProvisioningPlan.AttributeRequest attributeRequest : Util.safeIterable(accountRequest.getAttributeRequests())) {
197                if (findPredicate.test(accountRequest, attributeRequest)) {
198                    pairs.add(new Pair<>(accountRequest, attributeRequest));
199                }
200            }
201        }
202        return pairs;
203    }
204
205    /**
206     * Find pairs of account/attribute requests in the provisioning plan where the
207     * account matches the first filter and the account/attribute combined match
208     * the second filter.
209     *
210     * @param plan The provisioning plan
211     * @param accountFilter The account filter
212     * @param attributeFilter The attribute filter
213     * @return The list of account/attribute pairs
214     */
215    public static List<Pair<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest>> find(ProvisioningPlan plan, Predicate<ProvisioningPlan.AccountRequest> accountFilter, BiPredicate<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest> attributeFilter) {
216        BiPredicate<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest> combo = (a, b) -> {
217            if (accountFilter.test(a)) {
218                return attributeFilter.test(a, b);
219            }
220            return false;
221        };
222        return find(plan, combo);
223    }
224
225    /**
226     * Finds attribute requests matching any of the given names
227     *
228     * @param attributeName The attribute name(s)
229     * @return The predicate object
230     */
231    public static BiPredicate<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest> hasAttributeNames(final String... attributeName) {
232        final List<String> names = new ArrayList<>();
233        if (attributeName != null) {
234            names.addAll(Arrays.asList(attributeName));
235        }
236
237        return (accountRequest, attributeRequest) -> {
238            for(String name : names) {
239                if (Util.nullSafeEq(attributeRequest.getName(), name)) {
240                    return true;
241                }
242            }
243            return false;
244        };
245    }
246
247    /**
248     * Finds an attribute request matching the name, operation, and values as defined. Null
249     * and empty values are considered a "skip this match".
250     *
251     * @param attributeName The attribute name
252     * @param operation The operation to match
253     * @param value The value(s) to match
254     * @return The predicate object
255     */
256    public static BiPredicate<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest> hasAttributeRequest(final String attributeName, final ProvisioningPlan.Operation operation, final Object value) {
257        return (accountRequest, attributeRequest) -> {
258            if (Util.isNullOrEmpty(attributeName) || Util.nullSafeEq(attributeRequest.getName(), attributeName)) {
259                if (operation == null || Util.nullSafeEq(attributeRequest.getOperation(), operation)) {
260                    if (Utilities.isNothing(value) || Utilities.safeContainsAll(attributeRequest.getValue(), value)) {
261                        return true;
262                    } else if (Util.nullSafeEq(value, EMPTY_VALUE) && Utilities.isNothing(attributeRequest.getValue())) {
263                        return true;
264                    }
265                }
266            }
267            return false;
268        };
269    }
270
271    /**
272     * Remove all assigned entitlements (i.e. AttributeAssignments requested via LCM or
273     * added via certification) from the user, except those matched by the 'exceptFilter'.
274     */
275    public static void removeAssignedEntitlements(SailPointContext context, Identity target, ProvisioningPlan plan, Filter exceptFilter) throws GeneralException {
276        MapMatcher matcher = null;
277        if (exceptFilter != null) {
278            matcher = new MapMatcher(exceptFilter);
279        }
280        for(AttributeAssignment ra : Util.safeIterable(target.getAttributeAssignments())) {
281            if (matcher == null || !matcher.matches(Mapper.toMap(ra))) {
282                ProvisioningPlan.AccountRequest accountRequest = plan.getAccountRequest(ra.getApplicationName(), ra.getInstance(), ra.getNativeIdentity());
283                if (accountRequest == null) {
284                    accountRequest = new ProvisioningPlan.AccountRequest();
285                    accountRequest.setApplication(ra.getApplicationName());
286                    accountRequest.setNativeIdentity(ra.getNativeIdentity());
287                    accountRequest.setInstance(ra.getInstance());
288                    accountRequest.setOperation(ProvisioningPlan.AccountRequest.Operation.Modify);
289                    plan.add(accountRequest);
290                }
291                ProvisioningPlan.AttributeRequest attributeRequest = new ProvisioningPlan.AttributeRequest(ra.getName(), ProvisioningPlan.Operation.Remove, ra.getStringValue());
292                attributeRequest.setAssignment(true);
293                accountRequest.add(attributeRequest);
294            }
295        }
296    }
297
298    /**
299     * Remove all assigned roles from the given Identity by adding Remove operations to the
300     * given ProvisioningPlan. Role assignments matched by the 'exceptFilter' will not be removed.
301     */
302    public static void removeAssignedRoles(SailPointContext context, Identity target, ProvisioningPlan plan, Filter exceptFilter) throws GeneralException {
303        MapMatcher matcher = null;
304        if (exceptFilter != null) {
305            matcher = new MapMatcher(exceptFilter);
306        }
307        for(RoleAssignment ra : Util.safeIterable(target.getRoleAssignments())) {
308            if (matcher == null || !matcher.matches(Mapper.toMap(ra))) {
309                ProvisioningPlan.AccountRequest iiqRequest = plan.getIIQAccountRequest();
310                if (iiqRequest == null) {
311                    iiqRequest = new ProvisioningPlan.AccountRequest(ProvisioningPlan.AccountRequest.Operation.Modify, "IIQ", null, target.getName());
312                    plan.add(iiqRequest);
313                }
314                ProvisioningPlan.AttributeRequest removeRequest = new ProvisioningPlan.AttributeRequest("assignedRoles", ProvisioningPlan.Operation.Remove, ra.getRoleName());
315                removeRequest.setAssignmentId(ra.getAssignmentId());
316                iiqRequest.add(removeRequest);
317            }
318        }
319    }
320
321    /**
322     * Removes the given attribute request(s) matching by either name or operation.
323     *
324     * @param plan The plan to modify
325     * @param attributeName The attribute to remove (by name)
326     * @param attributeOperation The attribute to remove (by operation)
327     */
328    public static void removeAttributeRequest(ProvisioningPlan plan, String attributeName, ProvisioningPlan.Operation attributeOperation) {
329        removeAttributeRequest(plan, attributeName, attributeOperation, null);
330    }
331
332    /**
333     * Removes the given attribute request(s) matching by name.
334     *
335     * @param plan The plan to modify
336     * @param attributeName The attribute to remove (by name)
337     */
338    public static void removeAttributeRequest(ProvisioningPlan plan, String attributeName) {
339        removeAttributeRequest(plan, attributeName, null, null);
340    }
341
342    /**
343     * Removes the given attribute request(s) matching by either name, operation, or both, from any
344     * account requests on this plan. Nulls provided for any of the three criteria will skip matching that
345     * attribute.
346     *
347     * If you want to match an actual null or empty value, use {@link #EMPTY_VALUE}.
348     *
349     * @param plan The plan to modify
350     * @param attributeName The attribute name (possibly null) to match
351     * @param attributeOperation The attribute operation (possibly null) to match
352     * @param attributeValue The attribute value (possibly null) to match
353     */
354    public static void removeAttributeRequest(ProvisioningPlan plan, String attributeName, ProvisioningPlan.Operation attributeOperation, Object attributeValue) {
355        for(ProvisioningPlan.AccountRequest accountRequest : Util.safeIterable(plan.getAccountRequests())) {
356            if (accountRequest.getAttributeRequests() != null) {
357                removeAttributeRequest(accountRequest, attributeName, attributeOperation, attributeValue);
358            }
359        }
360    }
361
362    /**
363     * Removes the attribute requests matching by name, operation, and/or value from the given
364     * account request. Nulls provided for any of the three criteria will skip matching that
365     * attribute.
366     *
367     * @param accountRequest The account request to modify
368     * @param attributeName The attribute name
369     * @param attributeOperation The attribute operation
370     * @param attributeValue The attribute value
371     */
372    public static void removeAttributeRequest(ProvisioningPlan.AccountRequest accountRequest, String attributeName, ProvisioningPlan.Operation attributeOperation, Object attributeValue) {
373        // Copy to a temporary list to avoid concurrent modification exceptions
374        List<ProvisioningPlan.AttributeRequest> temporaryList = new ArrayList<>(accountRequest.getAttributeRequests());
375        for(ProvisioningPlan.AttributeRequest attributeRequest : temporaryList) {
376            if (Util.isNullOrEmpty(attributeName) || Util.nullSafeEq(attributeRequest.getName(), attributeName)) {
377                if (attributeOperation == null || Util.nullSafeEq(attributeRequest.getOperation(), attributeOperation)) {
378                    if (attributeValue == null || Util.nullSafeEq(attributeRequest.getValue(), attributeValue)) {
379                        accountRequest.remove(attributeRequest);
380                    } else if (Util.nullSafeEq(attributeValue, EMPTY_VALUE) && Utilities.isNothing(attributeRequest.getValue())) {
381                        accountRequest.remove(attributeRequest);
382                    }
383                }
384            }
385        }
386    }
387
388    /**
389     * Remove all entitlements from the given Identity by adding Remove operations to the
390     * given ProvisioningPlan. Entitlements matched by the 'exceptFilter' will not be removed.
391     */
392    public static void removeEntitlements(SailPointContext context, Identity target, ProvisioningPlan plan, Filter exceptFilter) throws GeneralException {
393        for(Link link : Util.safeIterable(target.getLinks())) {
394            Filter appFilter = Filter.eq("application.name", link.getApplicationName());
395            MapMatcher matcher = null;
396            if (exceptFilter != null) {
397                matcher = new MapMatcher(exceptFilter);
398            }
399            Attributes<String, Object> entitlements = link.getEntitlementAttributes();
400            if (entitlements != null) {
401                for(String name : entitlements.getKeys()) {
402                    List<String> values = entitlements.getStringList(name);
403                    for(String value : Util.safeIterable(values)) {
404                        QueryOptions qo = new QueryOptions();
405                        qo.addFilter(appFilter);
406                        qo.addFilter(Filter.eq("attribute", name));
407                        qo.addFilter(Filter.eq("value", value));
408                        qo.setResultLimit(1);
409                        ManagedAttribute ma = Utilities.safeSubscript(context.getObjects(ManagedAttribute.class, qo), 0);
410                        if (ma != null) {
411                            Map<String, Object> maMap = Mapper.toMap(link, ma);
412                            if (matcher == null || !matcher.matches(maMap)) {
413                                plan.add(link.getApplicationName(), link.getNativeIdentity(), name, ProvisioningPlan.Operation.Remove, value);
414                            }
415                        }
416                    }
417                }
418            }
419        }
420    }
421
422    /**
423     * Sets the given other attribute to the given value on any entitlement add.
424     */
425    public static void setOnEntitlementAdd(ProvisioningPlan plan, Application application, String otherAttribute, Object value) {
426        Schema appSchema = application.getAccountSchema();
427        boolean expandEnable = false;
428        ProvisioningPlan.AccountRequest template = null;
429        for(ProvisioningPlan.AccountRequest accountRequest : Util.safeIterable(plan.getAccountRequests(application.getName()))) {
430            for(ProvisioningPlan.AttributeRequest attributeRequest : Util.safeIterable(accountRequest.getAttributeRequests())) {
431                if (attributeRequest.getOperation().equals(ProvisioningPlan.Operation.Add) || attributeRequest.getOperation().equals(ProvisioningPlan.Operation.Set)) {
432                    String attributeName = attributeRequest.getName();
433                    AttributeDefinition attributeDefinition = appSchema.getAttributeDefinition(attributeName);
434                    if (attributeDefinition != null) {
435                        if (attributeDefinition.isEntitlement() || attributeDefinition.isManaged()) {
436                            expandEnable = true;
437                            template = accountRequest;
438                            break;
439                        }
440                    }
441                }
442            }
443        }
444        if (expandEnable) {
445            ProvisioningPlan.AccountRequest modify = new ProvisioningPlan.AccountRequest();
446            modify.setOperation(ProvisioningPlan.AccountRequest.Operation.Modify);
447            modify.setNativeIdentity(template.getNativeIdentity());
448            modify.setApplication(template.getApplicationName());
449            modify.setInstance(template.getInstance());
450            modify.setComments("Expand entitlement add to set " + otherAttribute);
451            modify.setArguments(template.getArguments());
452            ProvisioningPlan.AttributeRequest attr = new ProvisioningPlan.AttributeRequest(otherAttribute, ProvisioningPlan.Operation.Set, value);
453            modify.add(attr);
454            plan.add(modify);
455        }
456    }
457
458    /**
459     * Sorts the AccountRequests in the given ProvisioningPlan using the given sorter.
460     * Several useful sorters are provided in {@link PlanComparators}.
461     */
462    public static void sort(ProvisioningPlan plan, Comparator<ProvisioningPlan.AccountRequest> sorter) {
463        List<ProvisioningPlan.AccountRequest> requests = plan.getAccountRequests();
464        if (requests != null) {
465            requests.sort(sorter);
466            plan.setAccountRequests(requests);
467        }
468    }
469
470    /**
471     * Sorts the AccountRequests in the given ProvisioningPlan using the default order,
472     * which is defined in {@link PlanComparators#defaultSequence()}. Attributes on each
473     * AccountRequest are then sorted using the default attribute comparator.
474     *
475     * The order is roughly create, modify, status changes, delete. This is what most
476     * connectors are expecting when more than one operation happens at once.
477     */
478    public static void sort(ProvisioningPlan plan) {
479        sort(plan, PlanComparators.defaultSequence());
480        for(ProvisioningPlan.AccountRequest accountRequest : Util.safeIterable(plan.getAccountRequests())) {
481            sort(accountRequest);
482        }
483    }
484
485    /**
486     * Sorts the AttributeRequessts in the given ProvisioningPlan using the default order,
487     * which is defined in {@link PlanComparators#defaultAttributeSequence()}. The essence
488     * is removes first, then sets, then adds.
489     */
490    public static void sort(ProvisioningPlan.AccountRequest accountRequest) {
491        List<ProvisioningPlan.AttributeRequest> attributeRequests = accountRequest.getAttributeRequests();
492        if (attributeRequests != null) {
493            attributeRequests.sort(PlanComparators.defaultAttributeSequence());
494            accountRequest.setAttributeRequests(attributeRequests);
495        }
496    }
497
498}