001package com.identityworksllc.iiq.common.plugin;
002
003import com.identityworksllc.iiq.common.AbstractBaseUtility;
004import com.identityworksllc.iiq.common.Sameness;
005import com.identityworksllc.iiq.common.Syslogger;
006import com.identityworksllc.iiq.common.ThingAccessUtils;
007import com.identityworksllc.iiq.common.TooManyResultsException;
008import com.identityworksllc.iiq.common.Utilities;
009import com.identityworksllc.iiq.common.logging.LogCapture;
010import com.identityworksllc.iiq.common.logging.SLogger;
011import com.identityworksllc.iiq.common.plugin.annotations.AuthorizeAll;
012import com.identityworksllc.iiq.common.plugin.annotations.AuthorizeAny;
013import com.identityworksllc.iiq.common.plugin.annotations.AuthorizedBy;
014import com.identityworksllc.iiq.common.plugin.annotations.NoReturnValue;
015import com.identityworksllc.iiq.common.plugin.annotations.ResponsesAllowed;
016import com.identityworksllc.iiq.common.plugin.vo.ExpandedDate;
017import com.identityworksllc.iiq.common.plugin.vo.RestObject;
018import org.apache.commons.logging.LogFactory;
019import org.springframework.core.annotation.AnnotationUtils;
020import sailpoint.api.Matchmaker;
021import sailpoint.api.Meter;
022import sailpoint.api.ObjectUtil;
023import sailpoint.api.SailPointContext;
024import sailpoint.api.logging.SyslogThreadLocal;
025import sailpoint.authorization.Authorizer;
026import sailpoint.authorization.CapabilityAuthorizer;
027import sailpoint.authorization.UnauthorizedAccessException;
028import sailpoint.object.*;
029import sailpoint.rest.BaseResource;
030import sailpoint.rest.plugin.BasePluginResource;
031import sailpoint.tools.GeneralException;
032import sailpoint.tools.ObjectNotFoundException;
033import sailpoint.tools.Util;
034import sailpoint.tools.xml.AbstractXmlObject;
035
036import javax.faces.FactoryFinder;
037import javax.faces.component.UIViewRoot;
038import javax.faces.context.FacesContext;
039import javax.faces.context.FacesContextFactory;
040import javax.faces.lifecycle.Lifecycle;
041import javax.faces.lifecycle.LifecycleFactory;
042import javax.servlet.ServletContext;
043import javax.servlet.http.HttpServletRequest;
044import javax.servlet.http.HttpServletResponse;
045import javax.ws.rs.NotFoundException;
046import javax.ws.rs.container.ResourceInfo;
047import javax.ws.rs.core.Context;
048import javax.ws.rs.core.Response;
049import javax.ws.rs.core.Response.ResponseBuilder;
050import javax.ws.rs.core.Response.Status;
051import java.lang.reflect.Method;
052import java.text.SimpleDateFormat;
053import java.time.ZoneId;
054import java.time.format.TextStyle;
055import java.util.*;
056
057/**
058 * This class is the common base class for all IIQCommon-compliant plugin REST
059 * resources. It contains numerous enhancements over IIQ's original base plugin
060 * resource class, notably {@link #handle(PluginAction)}.
061 *
062 * See the provided documentation `PLUGIN-RESOURCES.adoc` for much more detail.
063 *
064 * Your plugins REST resource classes must extend this class to make use of its
065 * functions. The following is an example of handle():
066 *
067 * ```
068 * {@literal @}GET
069 * {@literal @}Path("endpoint/path")
070 * public Response endpointMethod(@QueryParam("param") String parameter) {
071 *     return handle(() -> {
072 *         // Your lambda body can do anything you need. Here we just call
073 *         // some example method.
074 *         List<String> output = invokeBusinessLogicHere();
075 *
076 *         // Will be automatically transformed into a JSON response
077 *         return output;
078 *     });
079 * }
080 * ```
081 */
082@SuppressWarnings("unused")
083public abstract class BaseCommonPluginResource extends BasePluginResource implements CommonExtendedPluginContext {
084
085        /**
086         * The interface that must be implemented by any plugin action passed
087         * to {@link #handle(Authorizer, PluginAction)}. In most cases, you will
088         * implement this as a lambda expression.
089         */
090        @FunctionalInterface
091        public interface PluginAction {
092                /**
093                 * Executes the action
094                 * @return Any values to return to the client
095                 * @throws Exception if any failure occur
096                 */
097                Object execute() throws Exception;
098        }
099
100        /**
101         * A hack to set the current FacesContext object. The method to set the current
102         * instance is "protected", so it can only be accessed from within a subclass.
103         */
104        private static abstract class InnerFacesContext extends FacesContext {
105                /**
106                 * Sets the current Faces Context within the context of this class,
107                 * which allows us access to a protected superclass static method
108                 * @param facesContext The new FacesContext
109                 */
110                protected static void setFacesContextAsCurrentInstance(FacesContext facesContext) {
111                        FacesContext.setCurrentInstance(facesContext);
112                }
113        }
114
115        /**
116         * Transforms a date response into a known format
117         *
118         * @param response The date response
119         * @return if any failures occur
120         */
121        private static Response transformDate(Date response) {
122                return Response.ok().entity(new ExpandedDate(response)).build();
123        }
124
125        /**
126         * If true, logs will be captured in handle() and appended to any error messages
127         */
128        private final ThreadLocal<Boolean> captureLogs;
129        /**
130         * The constructed FacesContext if there is one
131         */
132        private final ThreadLocal<FacesContext> constructedContext;
133        /**
134         * If true, logs will be captured in handle() and logged to the usual logger output even if they would not normally be
135         */
136        private final ThreadLocal<Boolean> forwardLogs;
137        /**
138         * An enhanced logger available to all plugins
139         */
140        protected final SLogger log;
141        /**
142         * Log messages captured to return with any errors
143         */
144        private final ThreadLocal<List<String>> logMessages;
145        /**
146         * A plugin authorization checker, if present
147         */
148        private PluginAuthorizationCheck pluginAuthorizationCheck;
149        /**
150         * Information about what resource is about to be invoked
151         */
152        @Context
153        protected ResourceInfo resourceInfo;
154        /**
155         * The {@link HttpServletResponse} associated with the current invocation
156         */
157        @Context
158        protected HttpServletResponse response;
159        /**
160         * The servlet context associated with the current invocation
161         */
162        @Context
163        protected ServletContext servletContext;
164        
165        /**
166         * The plugin resource
167         */
168        public BaseCommonPluginResource() {
169                this.log = new SLogger(LogFactory.getLog(this.getClass()));
170                this.logMessages = new ThreadLocal<>();
171                this.captureLogs = new ThreadLocal<>();
172                this.forwardLogs = new ThreadLocal<>();
173                this.constructedContext = new ThreadLocal<>();
174        }
175
176        /**
177         * The plugin resource
178         * @param parent The parent resource, used to inherit IIQ configuration
179         */
180        public BaseCommonPluginResource(BaseResource parent) {
181                super(parent);
182                this.log = new SLogger(LogFactory.getLog(this.getClass()));
183                this.logMessages = new ThreadLocal<>();
184                this.captureLogs = new ThreadLocal<>();
185                this.forwardLogs = new ThreadLocal<>();
186                this.constructedContext = new ThreadLocal<>();
187        }
188
189        /**
190         * Authorizes the given endpoint class and method according to the custom
191         * {@link AuthorizedBy}, {@link AuthorizeAll}, or {@link AuthorizeAny} annotations
192         * present on it.
193         *
194         * If authorization fails, an {@link UnauthorizedAccessException} will be thrown.
195         * Otherwise the method will return silently.
196         *
197         * If the logged in user is null, authorization silently succeeds.
198         *
199         * @param endpointClass The endpoint class from {@link ResourceInfo}
200         * @param endpointMethod The endpoint method from {@link ResourceInfo}
201         * @throws UnauthorizedAccessException if authorization fails
202         * @throws GeneralException if any other system failures occur during authorization
203         */
204        private void authorize(Class<?> endpointClass, Method endpointMethod) throws UnauthorizedAccessException, GeneralException {
205                Identity me = super.getLoggedInUser();
206                if (me == null) {
207                        return;
208                }
209
210                // NOTE: The difference between get and find is that find searches up the class
211                // hierarchy and get searches only the local level.
212                AuthorizedBy authorizedBy = AnnotationUtils.getAnnotation(endpointMethod, AuthorizedBy.class);
213                AuthorizeAll authorizeAll = null;
214                AuthorizeAny authorizeAny = null;
215
216                if (authorizedBy == null) {
217                        authorizeAll = AnnotationUtils.getAnnotation(endpointMethod, AuthorizeAll.class);
218                        authorizeAny = AnnotationUtils.getAnnotation(endpointMethod, AuthorizeAny.class);
219                }
220
221                if (authorizedBy == null && authorizeAll == null && authorizeAny == null) {
222                        authorizedBy = AnnotationUtils.findAnnotation(endpointClass, AuthorizedBy.class);
223
224                        if (authorizedBy == null) {
225                                authorizeAll = AnnotationUtils.findAnnotation(endpointClass, AuthorizeAll.class);
226                                authorizeAny = AnnotationUtils.findAnnotation(endpointClass, AuthorizeAny.class);
227                        }
228                }
229
230                if (authorizedBy != null) {
231                        if (!isAuthorized(authorizedBy, me)) {
232                                throw new UnauthorizedAccessException("User is not authorized to access this endpoint");
233                        }
234                } else if (authorizeAll != null) {
235                        if (authorizeAny != null) {
236                                throw new GeneralException("BAD CONFIGURATION: Endpoint method " + endpointClass.getName() + "." + endpointMethod.getName() + " is attached to both @AuthorizeAll and @AuthorizeAny annotations");
237                        }
238                        for(AuthorizedBy ab : authorizeAll.value()) {
239                                if (!isAuthorized(ab, me)) {
240                                        throw new UnauthorizedAccessException("User is not authorized to access this endpoint");
241                                }
242                        }
243                } else if (authorizeAny != null) {
244                        boolean match = false;
245                        for(AuthorizedBy ab : authorizeAny.value()) {
246                                if (isAuthorized(ab, me)) {
247                                        match = true;
248                                        break;
249                                }
250                        }
251                        if (!match) {
252                                throw new UnauthorizedAccessException("User is not authorized to access this endpoint");
253                        }
254                }
255        }
256
257        /**
258         * Builds a new FacesContext based on the HTTP request and response. The value
259         * returned by this method will be cleaned up (released) automatically after
260         * running the action in {@link #handle(PluginAction)}.
261         *
262         * See here: <a href="https://myfaces.apache.org/wiki/core/user-guide/jsf-and-myfaces-howtos/backend/access-facescontext-from-servlet.html">Access FacesContext from Servlet</a>
263         *
264         * @return A new FacesContext
265         */
266        private FacesContext buildFacesContext()
267        {
268                FacesContextFactory contextFactory = (FacesContextFactory)FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
269                LifecycleFactory lifecycleFactory = (LifecycleFactory)FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
270                Lifecycle lifecycle = lifecycleFactory.getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE);
271                FacesContext facesContext = contextFactory.getFacesContext(this.servletContext, request, response, lifecycle);
272                InnerFacesContext.setFacesContextAsCurrentInstance(facesContext);
273                UIViewRoot view = facesContext.getApplication().getViewHandler().createView(facesContext, "/login");
274                facesContext.setViewRoot(view);
275                constructedContext.set(facesContext);
276                return facesContext;
277        }
278
279        /**
280         * Checks access to the given "thing" and throws an {@link UnauthorizedAccessException} if access is not granted.
281         *
282         * @param thingAccessConfig The thing access configuration map
283         * @throws GeneralException if the access check fails for reasons unrelated to unauthorized access (e.g. script failure)
284         */
285        protected void checkThingAccess(Map<String, Object> thingAccessConfig) throws GeneralException {
286                checkThingAccess(null, thingAccessConfig);
287        }
288
289
290        /**
291         * Checks access to the given "thing" and throws an {@link UnauthorizedAccessException} if access is not granted
292         *
293         * @param target The target Identity, if any
294         * @param thingAccessConfig The thing access configuration map
295         * @throws GeneralException if the access check fails for reasons unrelated to unauthorized access (e.g. script failure)
296         */
297        protected void checkThingAccess(Identity target, Map<String, Object> thingAccessConfig) throws GeneralException {
298                if (!ThingAccessUtils.checkThingAccess(this, target, thingAccessConfig)) {
299                        throw new UnauthorizedAccessException();
300                }
301        }
302
303        /**
304         * Allows successful responses to be customized by overriding this method. By
305         * default, simply returns the input Response object.
306         * <p>
307         * Customizations should generally begin by invoking {@link Response#fromResponse(Response)}.
308         *
309         * @param actionResult The output of the {@link PluginAction} implementation in handle()
310         * @param restResponse The API output
311         */
312        protected Response customizeResponse(Object actionResult, Response restResponse) {
313                return restResponse;
314        }
315
316        /**
317         * Gets the attributes of either a {@link Configuration} or {@link Custom} object
318         * with the given name, in that order. If no such object exists, an empty Attributes
319         * object will be returned.
320         *
321         * This method should never return null.
322         *
323         * @param name Configuration name
324         * @return The attributes of the configuration
325         * @throws GeneralException if any lookup failures occur
326         */
327        public Attributes<String, Object> getConfiguration(String name) throws GeneralException {
328                Configuration c1 = getContext().getObjectByName(Configuration.class, name);
329                if (c1 != null && c1.getAttributes() != null) {
330                        return c1.getAttributes();
331                }
332                Custom c2 = getContext().getObjectByName(Custom.class, name);
333                if (c2 != null && c2.getAttributes() != null) {
334                        return c2.getAttributes();
335                }
336                return new Attributes<>();
337        }
338
339        /**
340         * Gets the configuration setting from the default plugin Configuration object or else from the plugin settings
341         * @param settingName The setting to retrieve
342         * @return The setting value
343         * @see #getConfigurationName()
344         */
345        @Override
346        public boolean getConfigurationBool(String settingName) {
347                return new ExtendedPluginContextHelper(getPluginName(), getContext(), this, this::getConfigurationName).getConfigurationBool(settingName);
348        }
349
350        /**
351         * Gets the configuration setting from the default plugin Configuration object or else from the plugin settings
352         * @param settingName The setting to retrieve
353         * @return The setting value
354         * @see #getConfigurationName()
355         */
356        @Override
357        public int getConfigurationInt(String settingName) {
358                return new ExtendedPluginContextHelper(getPluginName(), getContext(), this, this::getConfigurationName).getConfigurationInt(settingName);
359        }
360
361        /**
362         * Returns the name of the Configuration object for this plugin. It defaults to
363         * `Plugin Configuration [plugin name]`, but a subclass may want to override it.
364         *
365         * The configuration object named here is used by the following methods:
366         *
367         * * {@link #getConfigurationObject(String)}
368         * * {@link #getConfigurationBool(String)}
369         * * {@link #getConfigurationString(String)}
370         * * {@link #getConfigurationInt(String)}
371         *
372         * @return The name of the plugin configuration
373         */
374        protected String getConfigurationName() {
375                return "Plugin Configuration " + getPluginName();
376        }
377        
378        /**
379         * Gets the given configuration setting as an Object
380         * @param settingName The setting to retrieve as an Object
381         * @return The object
382         * @see #getConfigurationName()
383         */
384        @Override
385        public <T> T getConfigurationObject(String settingName) {
386                return (T)new ExtendedPluginContextHelper(getPluginName(), getContext(), this, this::getConfigurationName).getConfigurationObject(settingName);
387        }
388        
389        /**
390         * Gets the configuration setting from the default plugin Configuration object or else from the plugin settings
391         * @param settingName The setting to retrieve
392         * @return The setting value
393         * @see #getConfigurationName()
394         */
395        @Override
396        public String getConfigurationString(String settingName) {
397                return new ExtendedPluginContextHelper(getPluginName(), getContext(), this, this::getConfigurationName).getConfigurationString(settingName);
398        }
399        
400        /**
401         * Returns a distinct object matching the given filter. If the results are not distinct
402         * (i.e. more than one result is returned), a 400 error will be thrown. If there are no
403         * matching objects, a 404 error will be thrown.
404         *
405         * @param <T> The class to search for
406         * @param cls The class to search for
407         * @param filter A Filter for use with searching
408         * @return The resulting object (unless an error occurs)
409         * @throws GeneralException if an error occurs, or if there are no results, or if there are too many results
410         */
411        protected <T extends SailPointObject> T getDistinctObject(Class<T> cls, Filter filter) throws GeneralException {
412                QueryOptions qo = new QueryOptions();
413                qo.add(filter);
414                qo.setResultLimit(2);
415                List<T> results = getContext().getObjects(cls, qo);
416                if (results == null || results.size() == 0) {
417                        throw new ObjectNotFoundException(cls, filter.toString());
418                } else if (results.size() > 1) {
419                        throw new TooManyResultsException(cls, filter.toString(), results.size());
420                }
421                return results.get(0);
422        }
423
424        /**
425         * Returns a distinct object of the given type matching the given filter. If the results are not distinct (i.e. more than one result is returned), a 400 error will be thrown. If there are no matching objects, a 404 error will be thrown.
426         * @param <T> The class to search for
427         * @param cls The class to search for
428         * @param filter A QueryOptions for use with searching
429         * @return The resulting object (unless an error occurs)
430         * @throws GeneralException if an error occurs, or if there are no results, or if there are too many results
431         */
432        protected <T extends SailPointObject> T getDistinctObject(Class<T> cls, QueryOptions filter) throws GeneralException {
433                List<T> results = getContext().getObjects(cls, filter);
434                if (results == null || results.size() == 0) {
435                        throw new ObjectNotFoundException(cls, filter.toString());
436                } else if (results.size() > 1) {
437                        throw new TooManyResultsException(cls, filter.toString(), results.size());
438                }
439                return results.get(0);
440        }
441        
442        /**
443         * Get the standard exception mapping for output to the REST API caller. By
444         * default this will include information about the exception, the quick key
445         * used to look it up in the syslog query UI, and any log messages if log
446         * capturing is enabled.
447         *
448         * Subclasses may override this to add behavior, but most API clients written
449         * by IDW are expecting this output.
450         *
451         * @param t The exception to map
452         * @return The resulting map
453         */
454        protected Map<String, Object> getExceptionMapping(Throwable t) {
455                Map<String, Object> responseMap = CommonPluginUtils.getExceptionMapping(t, false);
456                responseMap.put("logs", logMessages.get());
457                return responseMap;
458        }
459        
460        /**
461         * Gets the current FacesContext if there is one or builds one if there is not.
462         * If the method needs to build a temporary FacesContext, it will be destroyed
463         * at the end of your `handle()` call to avoid memory leaks.
464         *
465         * @return A working FacesContext
466         */
467        protected FacesContext getFacesContext() {
468                FacesContext fc = FacesContext.getCurrentInstance();
469                if (fc != null) {
470                        return fc;
471                }
472                return buildFacesContext();
473        }
474        
475        /**
476         * A wrapper around {@link SailPointContext#getObject(Class, String)} that
477         * will throw a 404 exception if the search returns no records.
478         *
479         * @param <T> The class to search for
480         * @param cls The class to search for
481         * @param nameOrId The name or ID to search
482         * @return The object
483         * @throws ObjectNotFoundException if no results are found (results in a 404 in handle())
484         * @throws GeneralException if a search failure occurs
485         */
486        protected <T extends SailPointObject> T getObject(Class<T> cls, String nameOrId) throws GeneralException {
487                T object = getContext().getObject(cls, nameOrId);
488                if (object == null) {
489                        throw new ObjectNotFoundException(cls, nameOrId);
490                }
491                return object;
492        }
493        
494        /**
495         * A wrapper around {@link SailPointContext#getObject(Class, String)} that will throw a 404 exception if the search returns no records
496         * @param <T> The class to search for
497         * @param cls The class to search for
498         * @param nameOrId The name or ID to search
499         * @return The object
500         * @throws ObjectNotFoundException if no results are found (results in a 404 in handle())
501         * @throws GeneralException if a search failure occurs
502         */
503        protected <T extends SailPointObject> T getObjectById(Class<T> cls, String nameOrId) throws GeneralException {
504                T object = getContext().getObjectById(cls, nameOrId);
505                if (object == null) {
506                        throw new ObjectNotFoundException(cls, nameOrId);
507                }
508                return object;
509        }
510
511        /**
512         * A wrapper around {@link SailPointContext#getObject(Class, String)} that will throw a 404 exception if the search returns no records
513         * @param <T> The class to search for
514         * @param cls The class to search for
515         * @param nameOrId The name or ID to search
516         * @return The object
517         * @throws ObjectNotFoundException if no results are found (results in a 404 in handle())
518         * @throws GeneralException if a search failure occurs
519         */
520        protected <T extends SailPointObject> T getObjectByName(Class<T> cls, String nameOrId) throws GeneralException {
521                T object = getContext().getObjectByName(cls, nameOrId);
522                if (object == null) {
523                        throw new ObjectNotFoundException(cls, nameOrId);
524                }
525                return object;
526        }
527
528        /**
529         * Retrieves the {@link PluginAuthorizationCheck} previously set by a subclass.
530         * @return The configured PluginAuthorizationCheck, or null if none is set.
531         */
532        public PluginAuthorizationCheck getPluginAuthorizationCheck() {
533                return pluginAuthorizationCheck;
534        }
535
536        /**
537         * Safely retrieves the utility class in question. The utility class should implement a
538         * one-argument constructor taking a SailPointContext.
539         *
540         * @param <T> The utility type
541         * @param cls The class to retrieve
542         * @return An instance of the utility class
543         */
544        protected <T extends AbstractBaseUtility> T getUtility(Class<T> cls) {
545                try {
546                        return cls.getConstructor(SailPointContext.class).newInstance(getContext());
547                } catch(Exception e) {
548                        throw new IllegalArgumentException(cls.getName() + " does not appear to be a utility", e);
549                }
550        }
551
552        /**
553         * This method is responsible for executing the given action after checking the
554         * Authorizers. The action should be specified as a Java lambda expression.
555         *
556         * ```
557         * {@literal @}GET
558         * {@literal @}Path("endpoint/path")
559         * public Response endpointMethod(@QueryParam("param") String parameter) {
560         *     return handle(() -> {
561         *         List<String> output = invokeBusinessLogicHere();
562         *
563         *         // Will be automatically transformed into a JSON response
564         *         return output;
565         *     });
566         * }
567         * ```
568         *
569         * This method performs the following actions:
570         *
571         * 1) If log forwarding or capturing is enabled, switches it on for the current thread.
572         * 2) Starts a meter for checking performance of your action code.
573         * 3) Checks any Authorizers specified via the 'authorizer' parameter, class configuration, or an annotation.
574         * 4) Executes the provided {@link PluginAction}, usually a lambda expression.
575         * 5) Transforms the return value into a {@link Response}, handling both object output and thrown exceptions.
576         * 6) Finalizes the Meters and log capturing.
577         *
578         * If any authorizer fails, a 403 Forbidden response will be returned.
579         *
580         * @param authorizer If not null, the given authorizer will run first. A {@link Status#FORBIDDEN} {@link Response} will be returned if the authorizer fails
581         * @param action The action to execute, which should return the output of this API endpoint.
582         * @return The REST {@link Response}, populated according to the contract of this method
583         */
584        protected final Response handle(Authorizer authorizer, PluginAction action) {
585                if (resourceInfo != null) {
586                        Class<?> endpointClass = this.getClass();
587                        Method endpointMethod = resourceInfo.getResourceMethod();
588
589                        if (log.isTraceEnabled()) {
590                                log.trace("Entering handle() for REST API endpoint: Class = " + endpointClass.getName() + ", method = " + endpointMethod.getName());
591                        }
592                }
593                final String _meterName = "pluginRest:" + request.getRequestURI();
594                boolean shouldMeter = shouldMeter(request);
595                try {
596                        if (forwardLogs.get() != null && forwardLogs.get()) {
597                                LogCapture.addLoggers(this.getClass().getName());
598                                LogCapture.startInterception(message -> log.warn("{0}  {1} {2} - {3}", message.getDate(), message.getLevel(), message.getSource(), message.getMessage()));
599                        } else if (captureLogs.get() != null && captureLogs.get()) {
600                                LogCapture.addLoggers(this.getClass().getName());
601                                LogCapture.startInterception();
602                        }
603                        if (shouldMeter) {
604                                Meter.enter(_meterName);
605                        }
606                        try {
607                                boolean hasReturnValue = true;
608                                List<Class<?>> allowedReturnTypes = null;
609                                try {
610                                        if (resourceInfo != null) {
611                                                Class<?> endpointClass = this.getClass();
612                                                Method endpointMethod = resourceInfo.getResourceMethod();
613                                                authorize(endpointClass, endpointMethod);
614                                                if (endpointClass.isAnnotationPresent(NoReturnValue.class) || endpointMethod.isAnnotationPresent(NoReturnValue.class)) {
615                                                        hasReturnValue = false;
616                                                }
617                                                List<Class<?>> allowedClasses = new ArrayList<>();
618                                                if (endpointClass.isAnnotationPresent(ResponsesAllowed.class)) {
619                                                        Class<?>[] data = endpointClass.getAnnotation(ResponsesAllowed.class).value();
620                                                        if (data != null) {
621                                                                allowedClasses.addAll(Arrays.asList(data));
622                                                        }
623                                                }
624                                                if (endpointMethod.isAnnotationPresent(ResponsesAllowed.class)) {
625                                                        Class<?>[] data = endpointMethod.getAnnotation(ResponsesAllowed.class).value();
626                                                        if (data != null) {
627                                                                allowedClasses.addAll(Arrays.asList(data));
628                                                        }
629                                                }
630
631                                                if (!allowedClasses.isEmpty()) {
632                                                        allowedReturnTypes = allowedClasses;
633                                                        if (log.isTraceEnabled()) {
634                                                                log.trace("Allowed return value types: " + allowedReturnTypes);
635                                                        }
636                                                }
637                                        }
638                                        if (authorizer != null) {
639                                                // Method-level authorizer
640                                                authorize(authorizer);
641                                        }
642                                        if (pluginAuthorizationCheck != null) {
643                                                // Class-level default authorizer
644                                                pluginAuthorizationCheck.checkAccess();
645                                        }
646                                        if (this instanceof PluginAuthorizationCheck) {
647                                                ((PluginAuthorizationCheck) this).checkAccess();
648                                        }
649                                        if (this instanceof Authorizer) {
650                                                authorize((Authorizer)this);
651                                        }
652                                        Object actionResult = action.execute();
653
654                                        Response restResult;
655
656                                        try {
657                                                if (log.isTraceEnabled()) {
658                                                        log.trace("Entering user-defined handle() body");
659                                                }
660                                                restResult = handleResult(actionResult, hasReturnValue, allowedReturnTypes);
661                                        } finally {
662                                                if (log.isTraceEnabled()) {
663                                                        log.trace("Exiting user-defined handle() body");
664                                                }
665                                        }
666
667                                        return customizeResponse(actionResult, restResult);
668                                } catch(Exception e) {
669                                        // Log so that it makes it into the captured logs, if any exist
670                                        log.handleException(e);
671                                        Syslogger.logEvent(this.getClass(), "Error in REST API: " + e.getClass().getName(), e);
672                                        throw e;
673                                }
674                        } finally {
675                                logMessages.set(LogCapture.stopInterception());
676                                if (constructedContext.get() != null && !constructedContext.get().isReleased()) {
677                                        constructedContext.get().release();
678                                }
679                        }
680                } catch(UnauthorizedAccessException | SecurityException e) {
681                        return handleException(Response.status(Status.FORBIDDEN), e);
682                } catch(ObjectNotFoundException | NotFoundException e) {
683                        return handleException(Response.status(Status.NOT_FOUND), e);
684                } catch(IllegalArgumentException e) {
685                        return handleException(Response.status(Status.BAD_REQUEST), e);
686                } catch(Exception e) {
687                        return handleException(e);
688                } finally {
689                        if (shouldMeter) {
690                                Meter.exit(_meterName);
691                        }
692                        Meter.publishMeters();
693                        // Allow garbage collection
694            forwardLogs.remove();
695            captureLogs.remove();
696            logMessages.remove();
697            constructedContext.remove();
698                }
699        }
700
701        /**
702         * A wrapper method to handle plugin inputs. This is identical to invoking
703         * {@link #handle(Authorizer, PluginAction)} with a null Authorizer. See
704         * the documentation for that variant for more detail.
705         *
706         * Your {@link PluginAction} should be a Java lambda containing the actual
707         * business logic of your API endpoint. All method parameters will be available
708         * to it, as well as any class level attributes and effectively-final variables
709         * in the endpoint method.
710         *
711         * @param action The action to execute after passing authorization checks
712         * @return A valid JAX-RS {@link Response} object depending on the output of the PluginAction
713         */
714        protected final Response handle(PluginAction action) {
715                return handle(null, action);
716        }
717
718        /**
719         * Handles an exception by logging it and returning a Response with exception details
720         * @param t The exception to handle
721         * @return A JAX-RS response appropriate to the exception
722         */
723        protected final Response handleException(ResponseBuilder responseBuilder, Throwable t) {
724                Map<String, Object> responseMap = getExceptionMapping(t);
725                return responseBuilder.entity(responseMap).build();
726        }
727
728        /**
729         * Handles an exception by logging it and returning a Response with exception details
730         * @param t The exception to handle
731         * @return A JAX-RS response appropriate to the exception
732         */
733        protected Response handleException(Throwable t) {
734                return handleException(Response.serverError(), t);
735        }
736
737        /**
738         * Handles the result value by wrapping recognized objects into a Response and returning
739         * that. Response entities are processed via IIQ's {@link sailpoint.rest.jaxrs.JsonMessageBodyWriter},
740         * which passes them through the Flexjson library.
741         *
742         * @param hasReturnValue True unless the method specifies {@literal '@'}{@link NoReturnValue}
743         * @param response The output returned from the body of the handle method
744         * @return The resulting Response object
745         * @throws GeneralException if any failures occur processing the response
746         */
747        private Response handleResult(Object response, boolean hasReturnValue, List<Class<?>> allowedReturnTypes) throws GeneralException {
748                if (hasReturnValue) {
749                        if (response instanceof Response) {
750                                return ((Response) response);
751                        } else if (response instanceof ErrorResponse) {
752                                // Special wrapper allowing methods to return a non-OK, but still non-exceptional response
753                                ErrorResponse<?> errorResponse = (ErrorResponse<?>) response;
754                                Response metaResponse = handleResult(errorResponse.getWrappedObject(), true, allowedReturnTypes);
755
756                                return Response.fromResponse(metaResponse).status(errorResponse.getResponseCode()).build();
757                        } else if (response instanceof Map || response instanceof Collection || response instanceof String || response instanceof Number || response instanceof Enum) {
758                                return Response.ok().entity(response).build();
759                        } else if (response instanceof Date) {
760                                return transformDate((Date) response);
761                        } else if (response instanceof AbstractXmlObject) {
762                                Map<String, Object> responseMap = new HashMap<>();
763                                responseMap.put("xml", ((AbstractXmlObject) response).toXml());
764                                if (response instanceof SailPointObject) {
765                                        SailPointObject spo = ((SailPointObject) response);
766                                        responseMap.put("id", spo.getId());
767                                        responseMap.put("type", ObjectUtil.getTheRealClass(spo).getSimpleName());
768                                        responseMap.put("name", spo.getName());
769                                }
770                                return Response.ok().entity(responseMap).build();
771                        } else if (response instanceof RestObject) {
772                                return Response.ok().entity(response).build();
773                        } else if (response instanceof Exception) {
774                                return handleException((Exception) response);
775                        }
776
777                        if (response != null) {
778                                if (!Util.isEmpty(allowedReturnTypes)) {
779                                        for (Class<?> type : allowedReturnTypes) {
780                                                if (Utilities.isAssignableFrom(type, response.getClass())) {
781                                                        return Response.ok().entity(response).build();
782                                                }
783                                        }
784                                }
785                        }
786
787                        // NOTE: It is plausible that 'response' is null here. This is the only way to
788                        // allow both null and non-null outputs from the same REST API method.
789                        if (isAllowedOutput(response)) {
790                                return Response.ok().entity(response).build();
791                        }
792
793                        if (response == null) {
794                                log.warn("REST API output is null, but null outputs were not explicitly allowed with @NoReturnValue or isAllowedOutput()");
795                        } else {
796                                log.warn("REST API output type is not recognized: " + response.getClass());
797                        }
798                }
799                return Response.ok().build();
800        }
801        
802        /**
803         * A method allowing subclasses to specify whether a particular output object that
804         * is not part of the default set can be serialized and returned. This can be used
805         * when you don't have control of the object to extend RestObject.
806         *
807         * A subclass should extend this method and return true if a particular object is
808         * of an expected and supported type.
809         *
810         * @param response The response object
811         * @return True if the object should be accepted as valid output
812         */
813        protected boolean isAllowedOutput(@SuppressWarnings("unused") Object response) {
814                return false;
815        }
816        
817        /**
818         * Checks the provided annotation to see whether the given Identity is authorized
819         * per that annotation's criteria. Only one criterion may be specified per annotation.
820         * If more than one is specified, the highest criterion on this list will be the one
821         * used:
822         *
823         *  * systemAdmin
824         *  * right
825         *  * rights
826         *  * capability
827         *  * capabilities
828         *  * authorizerClass
829         *  * attribute
830         *  * population
831         *  * authorizerRule
832         *
833         * @param annotation The annotation to check
834         * @param me The (non-null) identity to check
835         * @return True if the user is authorized by the annotation's criteria
836         * @throws GeneralException if any Sailpoint errors occur
837         * @throws NullPointerException if the provided Identity is null
838         */
839        protected final boolean isAuthorized(AuthorizedBy annotation, Identity me) throws GeneralException {
840                Objects.requireNonNull(me, "This method can only be used when someone is logged in");
841                boolean authorized = false;
842                Identity.CapabilityManager capabilityManager = me.getCapabilityManager();
843                List<Capability> caps = capabilityManager.getEffectiveCapabilities();
844                if (annotation.systemAdmin()) {
845                        authorized = Capability.hasSystemAdministrator(caps);
846                } else if (Util.isNotNullOrEmpty(annotation.right())) {
847                        authorized = capabilityManager.hasRight(annotation.right());
848                } else if (annotation.rightsList().length > 0) {
849                        for(String right : annotation.rightsList()) {
850                                if (capabilityManager.hasRight(right)) {
851                                        authorized = true;
852                                        break;
853                                }
854                        }
855                } else if (Util.isNotNullOrEmpty(annotation.capability())) {
856                        authorized = Capability.hasCapability(annotation.capability(), caps);
857                } else if (annotation.capabilitiesList().length > 0) {
858                        for(String capability : annotation.capabilitiesList()) {
859                                if (Capability.hasCapability(capability, caps)) {
860                                        authorized = true;
861                                        break;
862                                }
863                        }
864                } else if (!Authorizer.class.equals(annotation.authorizerClass())) {
865                        try {
866                                Class<? extends Authorizer> authorizerClass = annotation.authorizerClass();
867                                Authorizer authorizer = authorizerClass.getConstructor().newInstance();
868                                authorize(authorizer);
869                                authorized = true;
870                        } catch(GeneralException e) {
871                                /* Authorization failed, ignore this */
872                        } catch(Exception e) {
873                                log.warn("Error during authorizer construction", e);
874                        }
875                } else if (Util.isNotNullOrEmpty(annotation.attribute())) {
876                        String attributeName = annotation.attribute();
877                        Object attributeValue = me.getAttribute(attributeName);
878                        if (Util.isNotNullOrEmpty(annotation.attributeValue())) {
879                                String testValue = annotation.attributeValue();
880                                if (attributeValue instanceof List) {
881                                        @SuppressWarnings("unchecked")
882                                        List<Object> attributeValues = (List<Object>)attributeValue;
883                                        authorized = Util.nullSafeContains(attributeValues, testValue);
884                                }
885                                if (!authorized) {
886                                        if (Sameness.isSame(testValue, attributeValue, false)) {
887                                                authorized = true;
888                                        }
889                                }
890                        } else if (annotation.attributeValueIn().length > 0) {
891                                for(String testValue : annotation.attributeValueIn()) {
892                                        if (Sameness.isSame(testValue, attributeValue, false)) {
893                                                authorized = true;
894                                                break;
895                                        }
896                                }
897                        } else {
898                                throw new IllegalArgumentException("If an attribute is defined in an AuthorizedBy annotation, either an attributeValue or an attributeValueIn clause must also be present");
899                        }
900                } else if (Util.isNotNullOrEmpty(annotation.population())) {
901                        GroupDefinition population = getContext().getObject(GroupDefinition.class, annotation.population());
902                        if (population != null) {
903                                if (population.getFilter() != null) {
904                                        Matchmaker matcher = new Matchmaker(getContext());
905                                        IdentitySelector selector = new IdentitySelector();
906                                        selector.setPopulation(population);
907                                        authorized = matcher.isMatch(selector, me);
908                                } else {
909                                        log.warn("AuthorizedBy annotation specifies non-filter population " + annotation.population());
910                                }
911                        } else {
912                                log.warn("AuthorizedBy annotation specifies non-existent population " + annotation.population());
913                        }
914                } else if (Util.isNotNullOrEmpty(annotation.authorizerRule())) {
915                        Map<String, Object> ruleParams = new HashMap<>();
916                        ruleParams.put("resourceInfo", resourceInfo);
917                        ruleParams.put("identity", me);
918                        ruleParams.put("log", log);
919                        ruleParams.put("userContext", this);
920                        ruleParams.put("annotation", annotation);
921
922                        Rule theRule = getContext().getObject(Rule.class, annotation.authorizerRule());
923
924                        if (theRule != null) {
925                                Object output = getContext().runRule(theRule, ruleParams);
926                                authorized = Util.otob(output);
927                        } else {
928                                log.warn("Authorizer rule " + annotation.authorizerRule() + " not found");
929                        }
930                }
931                return authorized;
932        }
933
934        /**
935         * If true, we should capture logs
936         * @return True if we should capture logs
937         */
938        public Boolean isCaptureLogs() {
939                return captureLogs.get();
940        }
941
942        /**
943         * Should we forward logs to the regular log destination?
944         * @return If true, we should forward logs to the regular log destinations
945         */
946        public Boolean isForwardLogs() {
947                return forwardLogs.get();
948        }
949
950        /**
951         * Sets the log capture flag. If true, logs will be captured and attached to any
952         * error outputs. If the response is not an error, logs will not be returned.
953         *
954         * @param captureLogs The logs
955         */
956        public void setCaptureLogs(boolean captureLogs) {
957                this.captureLogs.set(captureLogs);
958        }
959
960        /**
961         * Sets the forward logs flag. If true, logs from other classes will be intercepted
962         * and forwarded to the Logger associated with this class.
963         *
964         * @param forwardLogs If true, we should forward logs
965         */
966        public void setForwardLogs(boolean forwardLogs) {
967                this.forwardLogs.set(forwardLogs);
968        }
969
970        /**
971         * Sets the plugin authorization checker.
972         *
973         * This method is intended to be used in the constructor of subclasses.
974         *
975         * @param pluginAuthorizationCheck The plugin authorization checker
976         */
977        public final void setPluginAuthorizationCheck(PluginAuthorizationCheck pluginAuthorizationCheck) {
978                this.pluginAuthorizationCheck = pluginAuthorizationCheck;
979        }
980
981        /**
982         * Returns true if we ought to meter API calls to this resource. Subclasses
983         * can override this method to do their own detection.
984         * @param request The inbound servlet request
985         */
986        protected boolean shouldMeter(HttpServletRequest request) {
987                return true;
988        }
989
990        /**
991         * Performs some validation against the input, throwing an IllegalArgumentException if
992         * the validation logic returns false.
993         * @param check The check to execute
994         * @throws IllegalArgumentException if the check fails (returns false) or throws an exception
995         */
996        protected final void validate(PluginValidationCheck check) throws IllegalArgumentException {
997                validate(null, check);
998        }
999
1000        /**
1001         * Performs some validation against the input, throwing an IllegalArgumentException if
1002         * the validation logic returns false. The exception will contain the failure message by
1003         * default. You will provide a {@link PluginValidationCheck} implementation, typically
1004         * a lambda within your REST API entry point.
1005         *
1006         * If the validation check itself throws an exception, the output is the same as if the
1007         * check did not pass, except that the exception will be logged.
1008         *
1009         * @param failureMessage The failure message, or null to use a default
1010         * @param check The check to execute
1011         * @throws IllegalArgumentException if the check fails (returns false) or throws an exception
1012         */
1013        protected final void validate(String failureMessage, PluginValidationCheck check) throws IllegalArgumentException {
1014                try {
1015                        boolean result = check.test();
1016                        if (!result) {
1017                                if (Util.isNotNullOrEmpty(failureMessage)) {
1018                                        throw new IllegalArgumentException(failureMessage);
1019                                } else {
1020                                        throw new IllegalArgumentException("Failed a validation check");
1021                                }
1022                        }
1023                } catch(Exception e) {
1024                        log.handleException(e);
1025                        if (Util.isNotNullOrEmpty(failureMessage)) {
1026                                throw new IllegalArgumentException(failureMessage, e);
1027                        } else {
1028                                throw new IllegalArgumentException(e);
1029                        }
1030                }
1031        }
1032}