001package com.identityworksllc.iiq.common;
002
003import sailpoint.api.ObjectUtil;
004import sailpoint.api.SailPointContext;
005import sailpoint.api.SailPointFactory;
006import sailpoint.object.Attributes;
007import sailpoint.object.Identity;
008import sailpoint.object.Link;
009import sailpoint.object.ProvisioningPlan;
010import sailpoint.tools.GeneralException;
011import sailpoint.tools.Util;
012
013import java.util.*;
014import java.util.function.Consumer;
015import java.util.function.Supplier;
016
017/**
018 * A fluent builder for provisioning plans, especially useful for quickly writing
019 * rules or test cases. Import the methods from this class statically and begin with
020 * one of the plan() methods. The builder model will enforce only calling appropriate
021 * operations in any given context.
022 *
023 * The interfaces nested within this class will be used to implement a chain of
024 * modifiers to the 'current' object.
025 *
026 * Example:
027 *
028 * ```
029 * ProvisioningPlan plan =
030 *   PlanBuilder.plan(existingLink,
031 *     removeAllValues(existingLink, "emails"),
032 *     attribute("firstName", set("John")));
033 * ```
034 *
035 * This results in a ProvisioningPlan containing an AccountRequest to modify the
036 * given account. The AccountRequest contains AttributeRequests to remove all
037 * existing values from 'email' and to set the 'firstName' attribute to 'John'.
038 */
039public class PlanBuilder {
040
041    /**
042     * Interface for modifying existing links, can be passed to the Link plan() method
043     * for better fluency
044     */
045    public interface ExistingLinkModifier {}
046
047    /**
048     * Can modify the given provisioning plan
049     */
050    public interface PlanModifier extends ExistingLinkModifier {
051        /**
052         * Modifies the given plan
053         * @param plan The plan to modify
054         * @throws GeneralException if anything goes wrong
055         */
056        void modifyPlan(ProvisioningPlan plan) throws GeneralException;
057    }
058
059    /**
060     * Can modify the given account request
061     */
062    public interface AccountRequestModifier extends ExistingLinkModifier {
063        void modifyAccountRequest(ProvisioningPlan.AccountRequest accountRequest);
064    }
065
066    /**
067     * Can modify the given attribute request
068     */
069    public interface AttributeRequestModifier {
070        void modifyAttributeRequest(ProvisioningPlan.AttributeRequest attributeRequest);
071    }
072
073    /**
074     * Can be used in any context
075     */
076    public interface AnyModifier extends PlanModifier, AccountRequestModifier, AttributeRequestModifier {
077
078    }
079
080    /**
081     * Handles the case of a string parameter to operation()
082     */
083    public interface OperationModifier extends AccountRequestModifier, AttributeRequestModifier {
084
085    }
086
087    /**
088     * returns a plan modifier that adds an account request to the plan
089     * @param modifiers Any modifiers to apply to the account request
090     * @return The plan modifier
091     */
092    public static PlanModifier account(AccountRequestModifier... modifiers) {
093        return (plan) -> {
094            ProvisioningPlan.AccountRequest accountRequest = new ProvisioningPlan.AccountRequest();
095            for(AccountRequestModifier modifier : safeIterable(modifiers)) {
096                modifier.modifyAccountRequest(accountRequest);
097            }
098            plan.add(accountRequest);
099        };
100    }
101
102    /**
103     * Returns an AccountRequestModifier that sets the application on the account request
104     * @param application The application name
105     * @return The AccountRequestModifier
106     */
107    public static AccountRequestModifier application(String application) {
108        return (ar) -> ar.setApplication(application);
109    }
110
111    /**
112     * Returns a modifier for any object that adds an argument to the object's `arguments` Map.
113     * This method works on plans, requests, and attributes.
114     *
115     * @param name The argument name
116     * @param value The argument value
117     * @return The modifier
118     */
119    public static AnyModifier argument(String name, Object value) {
120        return new AnyModifier() {
121            @Override
122            public void modifyAccountRequest(ProvisioningPlan.AccountRequest accountRequest) {
123                setArgument(accountRequest::getArguments, accountRequest::setArguments, name, value);
124            }
125
126            @Override
127            public void modifyAttributeRequest(ProvisioningPlan.AttributeRequest attributeRequest) {
128                setArgument(attributeRequest::getArguments, attributeRequest::setArguments, name, value);
129            }
130
131            @Override
132            public void modifyPlan(ProvisioningPlan plan) throws GeneralException {
133                setArgument(plan::getArguments, plan::setArguments, name, value);
134            }
135        };
136    }
137
138    /**
139     * Creates a modifier to add an AttributeRequest to the AccountRequest
140     * @param name The name of the attribute
141     * @param modifiers Any modifiers to apply to the attribute request
142     * @return the resulting modifier implementation
143     */
144    public static AccountRequestModifier attribute(String name, AttributeRequestModifier... modifiers) {
145        return (ar) -> {
146            ProvisioningPlan.AttributeRequest attributeRequest = new ProvisioningPlan.AttributeRequest();
147            attributeRequest.setName(name);
148
149            for(AttributeRequestModifier modifier : safeIterable(modifiers)) {
150                modifier.modifyAttributeRequest(attributeRequest);
151            }
152
153            ar.add(attributeRequest);
154        };
155    }
156
157    /**
158     * Locates an Enum constant case-insensitively by name and returns it
159     * @param enumClass The enum class
160     * @param value The value to find
161     * @return The matching enum constant
162     * @param <T> The enum type
163     * @throws IllegalArgumentException if no matching constant is found
164     */
165    private static <T extends Enum<T>> T findValue(Class<T> enumClass, String value) {
166        EnumSet<T> values = EnumSet.allOf(enumClass);
167        for(T val : values) {
168            if (Util.nullSafeCaseInsensitiveEq(val.name(), value)) {
169                return val;
170            }
171        }
172        throw new IllegalArgumentException("No such enum value in " + enumClass.getName() + ": " + value);
173    }
174
175    /**
176     * Sets the identity on the plan to the given identity
177     * @param identity The identity to set
178     * @return A plan modifier that changes the plan Identity
179     */
180    public static PlanModifier identity(Identity identity) {
181        return (plan) -> plan.setIdentity(identity);
182    }
183
184    /**
185     * Creates a plan modifier that sets the identity on the plan to the identity with the given name or ID
186     * @param idOrName The identity name or ID
187     * @return The plan modifier
188     * @throws GeneralException if the identity cannot be found
189     */
190    public static PlanModifier identity(String idOrName) throws GeneralException {
191        SailPointContext context = SailPointFactory.getCurrentContext();
192        Identity identity = context.getObject(Identity.class, idOrName);
193        return (plan) -> plan.setIdentity(identity);
194    }
195
196    /**
197     * Merges the new value into the list of values in the attribute request
198     * @param attributeRequest The attribute request to modify
199     * @param value The new value
200     */
201    private static void mergeValue(ProvisioningPlan.AttributeRequest attributeRequest, Object value) {
202        List<Object> values = new ArrayList<>();
203        Object existingValue = attributeRequest.getValue();
204        if (existingValue instanceof Collection) {
205            values.addAll((Collection<?>) existingValue);
206        } else if (existingValue != null) {
207            values.add(existingValue);
208        }
209        if (value instanceof Collection) {
210            values.addAll((Collection<?>)value);
211        } else {
212            values.add(value);
213        }
214        attributeRequest.setValue(values);
215    }
216
217    /**
218     * Creates an AccountRequestModifier that sets the native identity on the account request
219     * @param ni The native identity to set
220     * @return The AccountRequestModifier
221     */
222    public static AccountRequestModifier nativeIdentity(String ni) {
223        return (ar) -> ar.setNativeIdentity(ni);
224    }
225
226    /**
227     * Creates an OperationModifier that sets the operation on the account request or attribute request
228     * @param op The operation to set, as a string
229     * @return The OperationModifier
230     */
231    public static OperationModifier operation(String op) {
232        return new OperationModifier() {
233            @Override
234            public void modifyAccountRequest(ProvisioningPlan.AccountRequest accountRequest) {
235                ProvisioningPlan.AccountRequest.Operation operation = findValue(ProvisioningPlan.AccountRequest.Operation.class, op);
236                accountRequest.setOperation(operation);
237            }
238
239            @Override
240            public void modifyAttributeRequest(ProvisioningPlan.AttributeRequest attributeRequest) {
241                ProvisioningPlan.Operation operation = findValue(ProvisioningPlan.Operation.class, op);
242                attributeRequest.setOperation(operation);
243            }
244        };
245    }
246
247    /**
248     * Creates an AttributeRequestModifier that sets the operation on the attribute request
249     * @param operation The operation to set
250     * @return The AttributeRequestModifier
251     */
252    public static AttributeRequestModifier operation(ProvisioningPlan.Operation operation) {
253        return (at) -> at.setOperation(operation);
254    }
255
256    /**
257     * Creates an AccountRequestModifier that sets the operation on the account request
258     * @param operation The operation to set
259     * @return The AccountRequestModifier
260     */
261    public static AccountRequestModifier operation(ProvisioningPlan.AccountRequest.Operation operation) {
262        return (ar) -> ar.setOperation(operation);
263    }
264
265    /**
266     * Creates a provisioning plan based on the given link and any modifiers.
267     * This method is one of the entry points to the PlanBuilder.
268     *
269     * @param toModify The link to modify
270     * @param modifiers Any modifiers to apply to the plan or account request
271     * @return The resulting plan
272     * @throws GeneralException if anything goes wrong
273     */
274    public static ProvisioningPlan plan(Link toModify, ExistingLinkModifier... modifiers) throws GeneralException {
275        ProvisioningPlan plan = new ProvisioningPlan();
276        if (toModify.getIdentity() != null) {
277            plan.setIdentity(toModify.getIdentity());
278        } else {
279            SailPointContext context = SailPointFactory.getCurrentContext();
280            String identityName = ObjectUtil.getIdentityFromLink(context, toModify.getApplication(), toModify.getInstance(), toModify.getNativeIdentity());
281            Identity identity = context.getObject(Identity.class, identityName);
282            plan.setIdentity(identity);
283        }
284
285        ProvisioningPlan.AccountRequest request = new ProvisioningPlan.AccountRequest();
286        request.setApplication(toModify.getApplicationName());
287        request.setNativeIdentity(toModify.getNativeIdentity());
288
289        for(ExistingLinkModifier modifier : safeIterable(modifiers)) {
290            if (modifier instanceof PlanModifier) {
291                ((PlanModifier) modifier).modifyPlan(plan);
292            } else if (modifier instanceof AccountRequestModifier) {
293                ((AccountRequestModifier) modifier).modifyAccountRequest(request);
294            }
295        }
296
297        plan.add(request);
298
299        return plan;
300    }
301
302    /**
303     * Creates a provisioning plan based on the given modifiers.
304     * This method is one of the entry points to the PlanBuilder.
305     *
306     * @param planModifiers Any modifiers to apply to the plan
307     * @return The resulting plan
308     * @throws GeneralException if anything goes wrong
309     */
310    public static ProvisioningPlan plan(PlanModifier... planModifiers) throws GeneralException {
311        ProvisioningPlan plan = new ProvisioningPlan();
312        for(PlanModifier modifier : safeIterable(planModifiers)) {
313            modifier.modifyPlan(plan);
314        }
315        return plan;
316    }
317
318    /**
319     * Generates a complex AccountRequestModifier that removes all existing values from the given field
320     * @param existing The Link from which to extract existing values
321     * @param field The field to remove values from
322     * @return The AccountRequestModifier
323     */
324    public static AccountRequestModifier removeAllValues(Link existing, String field) {
325        return (ar) -> {
326            if (existing == null || existing.getAttributes() == null) {
327                return;
328            }
329            ProvisioningPlan.AttributeRequest attributeRequest = ar.getAttributeRequest(field);
330            if (attributeRequest == null) {
331                attributeRequest = new ProvisioningPlan.AttributeRequest();
332                attributeRequest.setName(field);
333                attributeRequest.setOperation(ProvisioningPlan.Operation.Remove);
334            }
335            List<String> existingValues = existing.getAttributes().getStringList(field);
336            mergeValue(attributeRequest, existingValues);
337        };
338    }
339
340    /**
341     * Internal utility to create a safe Iterable object from an array
342     * which may be null or empty.
343     *
344     * @param array The array, which may be null
345     * @param <T> The type of the array
346     * @return A non-null Iterable object for use in a for loop
347     */
348    private static <T> Iterable<T> safeIterable(T[] array) {
349        if (array == null || array.length == 0) {
350            return new ArrayList<>();
351        }
352        return Arrays.asList(array);
353    }
354
355    /**
356     * Shortcut for 'operation("Set"), value(value)'
357     * @param value The value to set in the attribute request
358     * @return The AttributeRequestModifier
359     */
360    public static AttributeRequestModifier set(Object value) {
361        return (at) -> {
362            at.setOperation(ProvisioningPlan.Operation.Set);
363            if (value instanceof Identity) {
364                at.setValue(((Identity) value).getName());
365            } else {
366                at.setValue(value);
367            }
368        };
369    }
370
371    /**
372     * Utility method to retrieve the arguments from the given object, add the
373     * argument specified to the arguments, then put the arguments back. This
374     * generically handles the case where the arguments do not yet exist.
375     *
376     * @param getArguments A reference to the object's getArguments
377     * @param setArguments A reference to the object's setArguments
378     * @param arg The argument to set
379     * @param val The value of the argument
380     */
381    private static void setArgument(Supplier<Attributes<String, Object>> getArguments, Consumer<Attributes<String, Object>> setArguments, String arg, Object val) {
382        Attributes<String, Object> attributes = getArguments.get();
383        if (attributes == null) {
384            attributes = new Attributes<>();
385        }
386        attributes.put(arg, val);
387        setArguments.accept(attributes);
388    }
389
390    /**
391     * Adds the given value(s) to the Plan. If only one value is passed, it
392     * will be added as a single value. If value() is called more than once,
393     * if more than one value is passed, or if the attribute request already
394     * has a value, it will be transformed into a list and merged.
395     *
396     * @param values The values
397     * @return The input
398     */
399    public static AttributeRequestModifier value(Object... values) {
400        return (at) -> {
401            if (values != null && values.length == 1 && at.getValue() == null) {
402                at.setValue(values[0]);
403            } else {
404                for (Object val : safeIterable(values)) {
405                    mergeValue(at, val);
406                }
407            }
408        };
409    }
410
411}