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}