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}