001package com.identityworksllc.iiq.common.plugin; 002 003import com.identityworksllc.iiq.common.service.ServiceUtils; 004import org.apache.commons.logging.Log; 005import org.apache.commons.logging.LogFactory; 006import sailpoint.api.SailPointContext; 007import sailpoint.api.logging.SyslogThreadLocal; 008import sailpoint.object.Attributes; 009import sailpoint.object.Filter; 010import sailpoint.object.QueryOptions; 011import sailpoint.object.Server; 012import sailpoint.object.ServiceDefinition; 013import sailpoint.server.ServicerUtil; 014import sailpoint.tools.GeneralException; 015import sailpoint.tools.Util; 016 017import javax.servlet.http.HttpServletRequest; 018import javax.ws.rs.core.Response; 019import java.io.IOException; 020import java.io.PrintWriter; 021import java.io.StringWriter; 022import java.util.*; 023 024/** 025 * Some utilities to avoid boilerplate 026 */ 027@SuppressWarnings("unused") 028public class CommonPluginUtils { 029 /** 030 * The executor passed to {@link #singleServerExecute(SailPointContext, ServiceDefinition, SingleServerExecute)} 031 * mainly so that we can extend it with those default methods and throw an exception 032 * from run(). None of the out of box functional interfaces have exception throwing. 033 * 034 * You can use this either as a lambda or by implementing this interface in 035 * your Service class. 036 */ 037 @FunctionalInterface 038 public interface SingleServerExecute { 039 /** 040 * Wraps the implementation in start/stop timeout tracking code, saving 041 * those timestamps and the last run host on the ServiceDefinition after 042 * completion. This may be used for recurring services that need to know 043 * when they last ran (e.g., to do an incremental action). 044 * 045 * @param target The target ServiceDefinition to update 046 * @return The wrapped functional interface object 047 */ 048 @Deprecated 049 default SingleServerExecute andSaveTimestamps(ServiceDefinition target) { 050 return (context) -> { 051 long lastStart = System.currentTimeMillis(); 052 try { 053 this.singleServerExecute(context); 054 } finally { 055 ServiceUtils.storeTimestamps(context, target, lastStart); 056 } 057 }; 058 } 059 060 /** 061 * The main implementation of this service 062 * @param context The sailpoint context for the current run 063 * @throws GeneralException if any failures occur 064 */ 065 void singleServerExecute(SailPointContext context) throws GeneralException; 066 } 067 /** 068 * Log 069 */ 070 private static final Log log = LogFactory.getLog(CommonPluginUtils.class); 071 072 /** 073 * Utility classes should not be constructed 074 */ 075 private CommonPluginUtils() { 076 077 } 078 079 /** 080 * Attempts to find the Client IP, either via the request header X-FORWARDED-FOR (set 081 * by load balancers and reverse proxies), or via the request itself. 082 * 083 * @param request The {@link HttpServletRequest} object 084 * @return The client IP in an {@link Optional}, if it's available 085 */ 086 public static Optional<String> getClientIP(HttpServletRequest request) { 087 String remoteAddr = null; 088 if (request != null) { 089 remoteAddr = request.getHeader("X-FORWARDED-FOR"); 090 if (Util.isNullOrEmpty(remoteAddr)) { 091 remoteAddr = request.getRemoteAddr(); 092 } 093 } 094 return Optional.ofNullable(remoteAddr); 095 } 096 097 /** 098 * Gets the exception mapping 099 * @param t The exception to convert into a Map 100 * @param includeStackTrace True if we should include the stack trace in the response 101 * @return The exception transformed into a mapping 102 */ 103 public static Map<String, Object> getExceptionMapping(Throwable t, boolean includeStackTrace) { 104 Objects.requireNonNull(t); 105 106 Map<String, Object> responseMap = new HashMap<>(); 107 responseMap.put("exception", t.getClass().getName()); 108 responseMap.put("message", t.getMessage()); 109 responseMap.put("quickKey", SyslogThreadLocal.get()); 110 if (t.getCause() != null) { 111 responseMap.put("parentException", t.getCause().getClass().getName()); 112 responseMap.put("parentMessage", t.getCause().getMessage()); 113 } 114 if (includeStackTrace) { 115 try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { 116 t.printStackTrace(pw); 117 118 pw.flush(); 119 120 responseMap.put("stackTrace", sw.toString()); 121 } catch(IOException ignored) { 122 /* Swallow this */ 123 } 124 } 125 return responseMap; 126 } 127 128 /** 129 * Executes the task given by the functional {@link SingleServerExecute} if this 130 * server is the alphabetically lowest active server on which this Service is allowed 131 * to run. Server names are sorted by the database using an 'order by' on query. 132 * 133 * This is intended to be used as the bulk of the execute() method of a Service 134 * class. You can either pass a lambda/closure to this method or implement the 135 * SingleServerExecute interface in your Service class (in which case you'd 136 * simply pass 'this'). 137 * 138 * @param context The context 139 * @param self The current ServiceDefinition 140 * @param executor The executor to run 141 * @throws GeneralException if any failures occur 142 */ 143 public static void singleServerExecute(SailPointContext context, ServiceDefinition self, SingleServerExecute executor) throws GeneralException { 144 Server target = null; 145 QueryOptions qo = new QueryOptions(); 146 qo.addOrdering("name", true); 147 if (!Util.nullSafeCaseInsensitiveEq(self.getHosts(), "global")) { 148 qo.addFilter(Filter.in("name", Util.csvToList(self.getHosts()))); 149 } 150 List<Server> servers = context.getObjects(Server.class, qo); 151 for(Server s : servers) { 152 if (!s.isInactive() && ServicerUtil.isServiceAllowedOnServer(context, self, s.getName())) { 153 target = s; 154 break; 155 } 156 } 157 if (target == null) { 158 // This would be VERY strange, since we are, in fact, running the service 159 // right now, in this very code 160 log.warn("There does not appear to be an active server allowed to run service " + self.getName()); 161 } 162 String hostname = Util.getHostName(); 163 if (target == null || target.getName().equals(hostname)) { 164 executor.singleServerExecute(context); 165 } 166 } 167 168 /** 169 * Gets a map / JSON object indicating a status response 170 * @param message The message to associate with the response 171 * @return The response object 172 */ 173 public static Map<String, String> toStatusResponse(String message) { 174 return toStatusResponse(message, null); 175 } 176 177 /** 178 * Gets a map / JSON object indicating a status response with an optional error 179 * @param message The message to associate with the response 180 * @param error The error to associate with the response 181 * @return The response object 182 */ 183 public static Map<String, String> toStatusResponse(String message, Throwable error) { 184 Map<String, String> result = new TreeMap<>(); 185 result.put("message", message); 186 if (error != null) { 187 result.put("exception", error.toString()); 188 if (error.getCause() != null) { 189 result.put("exceptionImmediateCause", error.getCause().toString()); 190 } 191 } 192 return result; 193 } 194}