001package com.identityworksllc.iiq.common;
002
003import java.util.*;
004
005import javax.servlet.http.HttpSession;
006
007import com.fasterxml.jackson.annotation.JsonAutoDetect;
008import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
009import lombok.Getter;
010import lombok.Setter;
011import lombok.ToString;
012import org.apache.commons.logging.LogFactory;
013
014import com.identityworksllc.iiq.common.logging.SLogger;
015
016import sailpoint.api.SailPointContext;
017import sailpoint.api.SailPointFactory;
018import sailpoint.authorization.UnauthorizedAccessException;
019import sailpoint.object.AuditEvent;
020import sailpoint.object.Configuration;
021import sailpoint.object.Identity;
022import sailpoint.object.QuickLink;
023import sailpoint.service.MessageDTO;
024import sailpoint.service.quicklink.QuickLinkLauncher;
025import sailpoint.service.quicklink.QuickLinkLauncher.QuickLinkLaunchResult;
026import sailpoint.tools.GeneralException;
027import sailpoint.tools.JsonHelper;
028import sailpoint.tools.Util;
029
030/**
031 * Utilities for the JSP-based quicklink launcher service
032 */
033public class JSPUtils {
034
035    /**
036     * A value holder for the details of a quicklink launch attempt
037     */
038    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
039    @JsonIgnoreProperties(ignoreUnknown = true)
040    @Getter
041    @Setter
042    @ToString
043    public static final class QuicklinkLaunchKey {
044        /**
045         * the Identity ID or name of the launcher
046         */
047        private String launcher;
048
049        /**
050         * The name of the quicklink to launch
051         */
052        private String quicklinkName;
053
054        /**
055         * the Identity ID or name of the target
056         */
057        private String target;
058
059        /**
060         * The timestamp of the launch key generation
061         */
062        private long timestamp;
063
064        /**
065         * Checks if the launch key has expired
066         * @return true if the key has expired, false otherwise
067         */
068        public boolean isExpired() {
069            return System.currentTimeMillis() > timestamp + EXPIRATION_DURATION_MS;
070        }
071    }
072
073    /**
074     * The duration in milliseconds before a quicklink launch key expires (3 minutes)
075     */
076    private static final long EXPIRATION_DURATION_MS = 1000L * 60 * 3;
077
078    /**
079     * The logger for this class
080     */
081    private static final SLogger log = new SLogger(LogFactory.getLog(JSPUtils.class));
082
083    /**
084     * Checks whether the given launcher has access to launch the quicklink against
085     * the given target.
086     *
087     * @param context The IIQ context
088     * @param targetIdentity The target of the quicklink launch
089     * @param launcherIdentity The launcher of the quicklink
090     * @param quicklinkName The name of the quicklink to launch
091     * @param ql The QuickLink object
092     * @throws UnauthorizedAccessException if the access is not allowed
093     * @throws GeneralException if an error occurs during the access check
094     */
095    private static void checkAccess(SailPointContext context, Identity targetIdentity, Identity launcherIdentity, String quicklinkName, QuickLink ql) throws GeneralException {
096        AuthUtilities.QuickLinkAccessType qlAccessType;
097        if (targetIdentity.getName().equals(launcherIdentity.getName())) {
098            qlAccessType = AuthUtilities.QuickLinkAccessType.SELF;
099        } else {
100            qlAccessType = AuthUtilities.QuickLinkAccessType.OTHER;
101        }
102        AuthUtilities authUtils = new AuthUtilities(context);
103
104        if (!authUtils.canAccessQuicklink(launcherIdentity, targetIdentity, ql, qlAccessType)) {
105            log.error("User {0} cannot access QuickLink '{1}' for target {2}", launcherIdentity.getDisplayableName(), quicklinkName, targetIdentity.getDisplayableName());
106            throw new UnauthorizedAccessException("User " + launcherIdentity.getName() + " cannot access QuickLink '" + quicklinkName + "' for target " + targetIdentity.getName());
107        }
108    }
109
110    /**
111     * Gets the input key for the quicklink launcher. You should expose
112     * a plugin endpoint that produces this key.
113     *
114     * @param context The IIQ context
115     * @param targetIdentity The target of the quicklink launch
116     * @param launcherIdentity The launcher of the quicklink
117     * @param quicklinkName The name of the quicklink to launch
118     * @return An encrypted JSON string containing the configuration
119     * @throws GeneralException if encryption goes wrong
120     */
121    public static String getLaunchKey(SailPointContext context, Identity targetIdentity, Identity launcherIdentity, String quicklinkName) throws GeneralException {
122        QuickLink ql = context.getObject(QuickLink.class, quicklinkName);
123
124        checkAccess(context, targetIdentity, launcherIdentity, quicklinkName, ql);
125
126        QuicklinkLaunchKey key = new QuicklinkLaunchKey();
127        key.setLauncher(launcherIdentity.getName());
128        key.setTarget(targetIdentity.getName());
129        key.setQuicklinkName(quicklinkName);
130        key.setTimestamp(System.currentTimeMillis());
131
132        log.info("Generating launch key for [launcher = {0}, target = {1}, quicklink = {2}]", launcherIdentity.getDisplayableName(), targetIdentity.getDisplayableName(), quicklinkName);
133
134        Utilities.withPrivateContext((privateContext) -> {
135            AuditEvent ae = new AuditEvent();
136            ae.setAction("iiqcJspQuicklinkKeyGen");
137            ae.setSource(launcherIdentity.getName());
138            ae.setTarget(targetIdentity.getName());
139            ae.setString1(quicklinkName);
140
141            ae.setAttribute("launcher", launcherIdentity.getDisplayableName());
142            ae.setAttribute("target", targetIdentity.getDisplayableName());
143        });
144
145        String json = JsonHelper.toJson(key);
146        return context.encrypt(json);
147    }
148
149    /**
150     * Gets the input key for the quicklink launcher. You should expose
151     * a plugin endpoint that produces this key.
152     *
153     * @param context The IIQ context
154     * @param target The target of the quicklink launch
155     * @param launcher The launcher of the quicklink
156     * @param quicklinkName The name of the quicklink to launch
157     * @return An encrypted JSON string containing the configuration
158     * @throws GeneralException if encryption goes wrong
159     */
160    public static String getLaunchKey(SailPointContext context, String target, String launcher, String quicklinkName) throws GeneralException {
161        if (Util.isNullOrEmpty(quicklinkName)) {
162            throw new GeneralException("Quicklink name is required");
163        }
164
165        if (Util.isNullOrEmpty(target)) {
166            throw new GeneralException("Target is required");
167        }
168
169        if (Util.isNullOrEmpty(launcher)) {
170            throw new GeneralException("Launcher is required");
171        }
172
173        Identity launcherIdentity = context.getObject(Identity.class, launcher);
174        Identity targetIdentity = context.getObject(Identity.class, target);
175
176        if (launcherIdentity == null) {
177            throw new GeneralException("Launcher identity does not exist");
178        }
179        if (targetIdentity == null) {
180            throw new GeneralException("Target identity does not exist");
181        }
182
183        return getLaunchKey(context, targetIdentity, launcherIdentity, quicklinkName);
184    }
185    /**
186     * The IIQ context to use for this utility instance
187     */
188    private final SailPointContext context;
189    /**
190     * The HTTP session associated with this utility instance
191     */
192    private final HttpSession session;
193
194    /**
195     * Constructs a new JSPUtils with an HttpSession and the default threadlocal context
196     * @param session The HTTP session
197     * @throws GeneralException if getting the current context fails
198     */
199    public JSPUtils(HttpSession session) throws GeneralException {
200        this(SailPointFactory.getCurrentContext(), session);
201    }
202
203    /**
204     * Constructs a new JSPUtils with the specified HttpSession and context
205     * @param context The IIQ context to use for this utility instance
206     * @param session The HTTP session
207     */
208    public JSPUtils(SailPointContext context, HttpSession session) {
209        this.context = context;
210        this.session = session;
211    }
212
213    /**
214     * Launches a quicklink for the QL, target, and launcher specified in the encrypted
215     * key value. If the quicklink launch produces a form work item, the user will be
216     * redirected to the commonWorkItem page.
217     *
218     * @param encryptedKey An encrypted configuration key, from {@link #getLaunchKey(SailPointContext, String, String, String)}
219     * @return The redirect URL
220     * @throws GeneralException if decryption or quicklink launch fails
221     */
222    public String launchQuicklink(String encryptedKey) throws GeneralException {
223        Configuration systemConfig = Configuration.getSystemConfig();
224
225        String baseUrl = (String) systemConfig.get("serverRootPath");
226        String redirect = baseUrl + "/home.jsf";
227
228        if (Util.isNullOrEmpty(encryptedKey)) {
229            log.error("Encrypted quicklink config is null or empty");
230            return redirect;
231        }
232
233        String decryptedKey = context.decrypt(encryptedKey);
234        if (log.isDebugEnabled()) {
235            log.debug("Decrypted quicklink config: {0}", decryptedKey);
236        }
237        if (!decryptedKey.startsWith("{")) {
238            log.error("Invalid format for input to quicklink launcher");
239            return redirect;
240        }
241
242        QuicklinkLaunchKey launchKey = JsonHelper.fromJson(QuicklinkLaunchKey.class, decryptedKey);
243
244        if (log.isDebugEnabled()) {
245            log.debug("Decrypted quicklink launch key: {0}", launchKey);
246        }
247
248        String launcher = launchKey.getLauncher();
249        String quicklinkName = launchKey.getQuicklinkName();
250        String target = launchKey.getTarget();
251
252        long timestampLong = launchKey.getTimestamp();
253        long expiration = timestampLong + (1000L * 60 * 3);
254
255        if (System.currentTimeMillis() > expiration) {
256            Date date = new Date(expiration);
257            log.error("Quicklink launch key expired at {0}", date);
258            return redirect;
259        }
260
261        if (Util.isNullOrEmpty(target)) {
262            target = context.getUserName();
263        }
264
265        QuickLink ql = context.getObject(QuickLink.class, quicklinkName);
266        if (ql == null) {
267            log.error("No such quicklink in JSP launch: {0}", quicklinkName);
268            return redirect;
269        }
270
271        Identity launcherIdentity = context.getObject(Identity.class, launcher);
272        Identity targetIdentity = context.getObject(Identity.class, target);
273
274        if (targetIdentity == null || launcherIdentity == null) {
275            log.error("Either target or launcher identity does not exist. Input values: {0} {1}", target, launcher);
276            return redirect;
277        }
278
279        log.debug("Attempting QuickLink launch: name = {0}, launcher = {1}, target = {2}", quicklinkName, launcherIdentity.getDisplayableName(), targetIdentity.getDisplayableName());
280
281        checkAccess(context, targetIdentity, launcherIdentity, quicklinkName, ql);
282
283        Utilities.withPrivateContext((privateContext) -> {
284            AuditEvent ae = new AuditEvent();
285            ae.setAction("iiqcJspQuicklinkLaunch");
286            ae.setSource(launcherIdentity.getName());
287            ae.setTarget(targetIdentity.getName());
288            ae.setString1(quicklinkName);
289
290            ae.setAttribute("launcher", launcherIdentity.getDisplayableName());
291            ae.setAttribute("target", targetIdentity.getDisplayableName());
292
293            privateContext.saveObject(ae);
294            privateContext.commitTransaction();
295        });
296
297        QuickLinkLauncher api = new QuickLinkLauncher(context, launcherIdentity);
298
299        // QuickLinkLauncher populates this object with output in launchWorkflow()
300        Map<String, Object> sessionMap = new HashMap<>();
301
302        List<String> ids = new ArrayList<>();
303        ids.add(targetIdentity.getId());
304        QuickLinkLaunchResult result = api.launchWorkflow(ql, targetIdentity.getId(), ids, sessionMap);
305
306        if (sessionMap.containsKey("workItemFwdNextPage")) {
307            // Copy the QuickLink output into the HTTP session
308            for (String key : sessionMap.keySet()) {
309                this.session.setAttribute(key, sessionMap.get(key));
310            }
311            redirect = baseUrl + "/workitem/commonWorkItem.jsf#/commonWorkItem/session";
312
313            if (result.getMessages() != null && !result.getMessages().isEmpty()) {
314                for(MessageDTO msg : result.getMessages()) {
315                    log.info("Message from quicklink {0} invoked by {1}: {2}", launcherIdentity.getDisplayableName(), ql.getName(), msg.getMessageOrKey());
316                }
317            }
318        }
319
320        return redirect;
321
322    }
323
324}