001package com.identityworksllc.iiq.common;
002
003import sailpoint.api.ObjectUtil;
004import sailpoint.api.RequestManager;
005import sailpoint.api.SailPointContext;
006import sailpoint.api.Workflower;
007import sailpoint.object.*;
008import sailpoint.request.WorkflowRequestExecutor;
009import sailpoint.tools.GeneralException;
010import sailpoint.tools.MapUtil;
011import sailpoint.tools.Message;
012import sailpoint.transformer.IdentityTransformer;
013import sailpoint.workflow.StandardWorkflowHandler;
014import sailpoint.workflow.WorkflowContext;
015
016import java.time.Instant;
017import java.time.LocalDate;
018import java.time.LocalDateTime;
019import java.time.ZoneId;
020import java.time.temporal.ChronoUnit;
021import java.util.*;
022import java.util.concurrent.TimeUnit;
023
024/**
025 * Workflow utilities
026 */
027@SuppressWarnings("unused")
028public class WorkflowUtilities extends AbstractBaseUtility {
029
030    /**
031     * An enumeration of available LCM workflow types and the system config property required to access them
032     */
033    @SuppressWarnings("javadoc")
034    public enum LCMWorkflowType {
035        ACCESS_REQUEST("AccessRequest"),
036        ACCOUNT_REQUEST("AccountsRequest"),
037        ENTITLEMENT_REQUEST("EntitlementsRequest"),
038        EXPIRE_PASSWORD("ExpirePassword"),
039        FORGOT_PASSWORD("ForgotPassword"),
040        IDENTITY_CREATE("IdentityCreateRequest"),
041        IDENTITY_EDIT("IdentityEditRequest"),
042        PASSWORD("PasswordsRequest"),
043        ROLES_REQUEST("RolesRequest"),
044        SELF_SERVICE_REGISTRATION("SelfServiceRegistrationRequest"),
045        UNLOCK_ACCOUNT("UnlockAccount");
046
047        /**
048         * The workflow configuration name
049         */
050        private final String workflowFlowName;
051
052        /**
053         * Constructs an enum constant
054         *
055         * @param flow The LCM flow name
056         */
057        LCMWorkflowType(String flow) {
058            this.workflowFlowName = flow;
059        }
060
061        /**
062         * Retrieves the workflow object associated with the given action
063         *
064         * @param context SailPoint context used to do the retrieval
065         * @return The workflow object to use for this LCM action
066         * @throws GeneralException if any failures occur
067         */
068        public Workflow getConfiguredWorkflow(SailPointContext context) throws GeneralException {
069            String workflowName = ObjectUtil.getLCMWorkflowName(context, getFlowName());
070            Workflow wf = null;
071            if (workflowName != null) {
072                wf = context.getObjectByName(Workflow.class, workflowName);
073            }
074            return wf;
075        }
076
077        /**
078         * Retrieves the workflow name associated with the given action
079         *
080         * @param context SailPoint context used to do the retrieval
081         * @return The workflow name to use for this LCM action
082         * @throws GeneralException if any failures occur
083         */
084        public String getConfiguredWorkflowName(SailPointContext context) throws GeneralException {
085            String wfName = ObjectUtil.getLCMWorkflowName(context, getFlowName());
086            if (wfName == null || wfName.isEmpty()) {
087                return "LCM Provisioning";
088            }
089            return wfName;
090        }
091
092        /**
093         * Retrieves the flow name associated with this workflow type
094         *
095         * @return The flow name
096         */
097        public String getFlowName() {
098            return this.workflowFlowName;
099        }
100    }
101
102    /**
103     * An enumeration of available non-LCM workflow types and the system property required to access them
104     * <p>
105     * TODO Finish this
106     */
107    @SuppressWarnings("javadoc")
108    public enum WorkflowType {
109        IDENTITY_REFRESH("IdentityRefresh"),
110        IDENTITY_UPDATE("IdentityUpdate");
111
112        private final String workflowFlowName;
113
114        WorkflowType(String flow) {
115            this.workflowFlowName = flow;
116        }
117
118        public String getFlowName() {
119            return this.workflowFlowName;
120        }
121    }
122
123    /**
124     * Constructor for workflow utilities
125     *
126     * @param c The current SailPointContext
127     */
128    public WorkflowUtilities(SailPointContext c) {
129        super(c);
130    }
131
132    /**
133     * Creates a new form model map with the required parameters set. If you
134     * need to add complex values to the Map, using {@link MapUtil#put(Map, String, Object)}
135     * is a great option.
136     *
137     * @return A new form model with the required parameters
138     */
139    public static Map<String, Object> createFormModel() {
140        Map<String, Object> model = new HashMap<>();
141        model.put(IdentityTransformer.ATTR_TRANSFORMER_CLASS, IdentityTransformer.class.getName());
142        model.put(IdentityTransformer.ATTR_TRANSFORMER_OPTIONS, "");
143        return model;
144    }
145
146    /**
147     * Launches the given workflow asynchronously, using a Request to launch the
148     * Workflow and associated TaskResult at a later time. If the launch time is
149     * not in the future, it will be set to the current System timestamp.
150     *
151     * @param wf         The workflow to launch
152     * @param caseName   The case name ot use
153     * @param parameters The parameters to the workflow
154     * @param launchTime The time at which the workflow will be launched (in milliseconds since epoch)
155     * @throws GeneralException if any IIQ error occurs
156     */
157    public void launchAsync(Workflow wf, String caseName, Map<String, Object> parameters, long launchTime) throws GeneralException {
158        long launchTimeFinal = Math.max(launchTime, System.currentTimeMillis());
159
160        Attributes<String, Object> reqArgs = new Attributes<>();
161        reqArgs.put(StandardWorkflowHandler.ARG_REQUEST_DEFINITION, WorkflowRequestExecutor.DEFINITION_NAME);
162        reqArgs.put(StandardWorkflowHandler.ARG_WORKFLOW, wf.getName());
163        reqArgs.put(StandardWorkflowHandler.ARG_REQUEST_NAME, caseName);
164        reqArgs.putAll(parameters);
165
166        Request req = new Request();
167        RequestDefinition reqdef = context.getObject(RequestDefinition.class, WorkflowRequestExecutor.DEFINITION_NAME);
168        req.setDefinition(reqdef);
169        req.setEventDate(new Date(launchTimeFinal));
170        req.setOwner(context.getObjectByName(Identity.class, "spadmin"));
171        req.setName(caseName);
172        req.setAttributes(reqdef, reqArgs);
173
174        RequestManager.addRequest(context, req);
175    }
176
177    /**
178     * Launches the given workflow asynchronously, using a Request to launch the
179     * Workflow and associated TaskResult at a later time. If the launch time is
180     * not in the future, it will be set to the current System timestamp.
181     *
182     * @param wf         The workflow to launch
183     * @param caseName   The case name ot use
184     * @param parameters The parameters to the workflow
185     * @param launchTime The time at which the workflow will be launched
186     * @throws GeneralException if any IIQ error occurs
187     */
188    public void launchAsync(Workflow wf, String caseName, Map<String, Object> parameters, LocalDate launchTime) throws GeneralException {
189        long launchTimeMillis = launchTime.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
190        launchAsync(wf, caseName, parameters, launchTimeMillis);
191    }
192
193    /**
194     * Launches the given workflow asynchronously, using a Request to launch the
195     * Workflow and associated TaskResult at a later time. If the launch time is
196     * not in the future, it will be set to the current System timestamp.
197     *
198     * @param wf         The workflow to launch
199     * @param caseName   The case name ot use
200     * @param parameters The parameters to the workflow
201     * @param launchTime The time at which the workflow will be launched
202     * @throws GeneralException if any IIQ error occurs
203     */
204    public void launchAsync(Workflow wf, String caseName, Map<String, Object> parameters, LocalDateTime launchTime) throws GeneralException {
205        long launchTimeMillis = launchTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
206        launchAsync(wf, caseName, parameters, launchTimeMillis);
207    }
208
209    /**
210     * Launches the given workflow with the given delay
211     *
212     * @param wf         The workflow to launch
213     * @param caseName   The case name ot use
214     * @param parameters The parameters to the workflow
215     * @param howMuch    How much to delay the launch of the workflow
216     * @param timeUnit   The time unit represented by the howMuch parameter
217     * @throws GeneralException if any IIQ error occurs
218     */
219    public void launchDelayed(Workflow wf, String caseName, Map<String, Object> parameters, long howMuch, TimeUnit timeUnit) throws GeneralException {
220        if (howMuch < 0) {
221            throw new IllegalArgumentException("The delay must be a positive number");
222        }
223        long launchTime = System.currentTimeMillis();
224        long delayMs = TimeUnit.MILLISECONDS.convert(howMuch, timeUnit);
225        long delayDays = TimeUnit.DAYS.convert(howMuch, timeUnit);
226        if (delayDays > 30) {
227            log.warn("Submitting workflow " + caseName + " with a delay greater than 30 days; is this what you want?");
228        }
229        launchTime += delayMs;
230        launchAsync(wf, caseName, parameters, launchTime);
231    }
232
233    /**
234     * Launches the given workflow with the given delay
235     *
236     * @param wf         The workflow to launch
237     * @param caseName   The case name ot use
238     * @param parameters The parameters to the workflow
239     * @param howMuch    How much to delay the launch of the workflow
240     * @param timeUnit   The time unit represented by the howMuch parameter
241     * @throws GeneralException if any IIQ error occurs
242     */
243    public void launchDelayed(Workflow wf, String caseName, Map<String, Object> parameters, long howMuch, ChronoUnit timeUnit) throws GeneralException {
244        if (howMuch < 0) {
245            throw new IllegalArgumentException("The delay must be a positive number");
246        }
247        long launchTime = System.currentTimeMillis();
248        Instant when = Instant.now().plus(howMuch, timeUnit);
249        long delayMs = when.toEpochMilli() - launchTime;
250        long delayDays = TimeUnit.DAYS.convert(delayMs, TimeUnit.MILLISECONDS);
251        if (delayDays > 30) {
252            log.warn("Submitting workflow " + caseName + " with a delay greater than 30 days; is this what you want?");
253        }
254        launchTime += delayMs;
255        launchAsync(wf, caseName, parameters, launchTime);
256    }
257
258    /**
259     * Internal method to launch a workflow using the RequestManager. Used for testing
260     * by overriding this method to do something different.
261     *
262     * @param context The SailPointContext to use for launching the workflow
263     * @param request The Request to launch
264     * @throws GeneralException if any IIQ error occurs
265     */
266    protected void launchInternal(SailPointContext context, Request request) throws GeneralException {
267        RequestManager.addRequest(context, request);
268    }
269
270
271    /**
272     * Launches the given workflow quietly (i.e. absorbs errors if there are any)
273     *
274     * @param wf         The workflow to launch
275     * @param caseName   The case name ot use
276     * @param parameters The parameters to the workflow
277     */
278    public void launchQuietly(Workflow wf, String caseName, Map<String, Object> parameters) {
279        try {
280            Workflower workflower = new Workflower(context);
281            workflower.launchSafely(wf, caseName, parameters);
282        } catch (GeneralException e) {
283            /* TODO log and ignore */
284        }
285    }
286
287    /**
288     * Sets the target workflow reference of a step to another workflow at runtime. This can be used for polymorphic behavior.
289     *
290     * @param wfcontext      The workflow to modify
291     * @param stepName       The step name to modify
292     * @param targetWorkflow The new target workflow for that step
293     * @throws GeneralException if any IIQ error occurs
294     */
295    public void setStepTargetWorkflow(WorkflowContext wfcontext, String stepName, String targetWorkflow) throws GeneralException {
296        Workflow.Step step = wfcontext.getWorkflow().getStep(stepName);
297        step.setSubProcess(context.getObjectByName(Workflow.class, targetWorkflow));
298
299        List<Workflow.Arg> arguments = new ArrayList<>();
300        Attributes<String, Object> variables = wfcontext.getWorkflow().getVariables();
301
302        for (String key : variables.getKeys()) {
303            if (!key.equals("targetWorkflow")) {
304                Workflow.Arg arg = new Workflow.Arg();
305                arg.setName(key);
306                arg.setValue("ref:" + key);
307                arguments.add(arg);
308            }
309        }
310
311        step.setArgs(arguments);
312    }
313
314    /**
315     * Appends an error message to the given workflow context / task result with a timestamp
316     *
317     * @param wfcontext The Workflow Context (wfcontext from any workflow step)
318     * @param message   The message to append
319     */
320    public void wfError(WorkflowContext wfcontext, String message) {
321        WorkflowCase wfcase = wfcontext.getWorkflowCase();
322        if (wfcase != null) {
323            wfcase.addMessage(Message.error(Utilities.timestamp() + ": " + message));
324        }
325    }
326
327    /**
328     * Appends a message to the given workflow context / task result with a timestamp
329     *
330     * @param wfcontext The Workflow Context (wfcontext from any workflow step)
331     * @param message   The message to append
332     */
333    public void wfMessage(WorkflowContext wfcontext, String message) {
334        WorkflowCase wfcase = wfcontext.getWorkflowCase();
335        if (wfcase != null) {
336            wfcase.addMessage(Utilities.timestamp() + ": " + message);
337        }
338    }
339
340    /**
341     * Appends a warning message to the given workflow context / task result with a timestamp
342     *
343     * @param wfcontext The Workflow Context (wfcontext from any workflow step)
344     * @param message   The message to append
345     */
346    public void wfWarn(WorkflowContext wfcontext, String message) {
347        WorkflowCase wfcase = wfcontext.getWorkflowCase();
348        if (wfcase != null) {
349            wfcase.addMessage(Message.warn(Utilities.timestamp() + ": " + message));
350        }
351    }
352}