August 28, 2024

IIQCommon overview: Eliminate boilerplate in plugin resources

This will be part of a blog series providing an overview of the many SailPoint IIQ utilities and tools available in Instrumental ID’s open-source iiq-common-public library. (For an overview of the entire library, see this earlier post.)

This post will be about the BaseCommonPluginResource, the superclass to nearly all of Instrumental Identity’s web services. This utility, born of Instrumental ID’s decade of experience creating IIQ plugins, allows you to eliminate boilerplate and write better REST API code.

You will need to download or build that library’s JAR file and include it in your IIQ installation, either at web/WEB-INF/lib in an SSB repository or at WEB-INF/lib directly in Tomcat.

Overview

When implementing a REST API resource for an IIQ plugin, it’s usually recommended to extend IIQ’s BasePluginResource. This superclass offers several plugin-related features, notably forcing you to declare the plugin name so that it can retrieve plugin configuration at runtime. However, it does not enforce any particular coding style, resulting in a wide array of implementations, and it requires that you interact with Response objects directly.

BaseCommonPluginResource is our extension of BasePluginResource. It offers the following features:

  • Enforces (or at least strongly suggests) a consistent structure for your web services methods via its handle() method.
  • Decouples your actual API logic from the business of writing a REST API endpoint, allowing you to simply return business objects or throw exceptions, rather than worrying about Response objects and error codes.
  • Automatically handles authorization (also via handle()), either via custom Authorizer instances or IIQCommon’s own annotations.
  • Ensures that the objects returned from web services are safe by rejecting unknown return objects.
  • Automatically handles serialization of outputs and exceptions to HTTP responses, with hooks to customize the behavior.
  • Allows capture of logs, so that all logs from the web service and a set of useful classes can optionally be returned to the client for debugging.
  • Automatically meters every web service for performance tracing.
  • Provides a number of utilities and injected servlet variables, which are then available to all of your web services.

Implementing a service with handle

The handle() method uses a Java lambda to wrap the actual business logic of your REST API method, which I will call the “body“, as in the following examples. Just write regular Java code, without worrying about the fact that you’re expected to return a Response. Return regular objects! Throw regular Exceptions!

@GET
@Path("plugins/list")
public Response listPlugins() {
    return handle(() -> {
        PluginListResponse output = new PluginListResponse();

        List<Plugin> plugins = getContext().getObjects(Plugin.class);
        for(Plugin p : Util.safeIterable(plugins)) {
            output.getPlugins().add(new PluginVO(p));
        }

        output.setClassloaderVersion(Environment.getEnvironment().getPluginsCache().getVersion());

        return output;
    });
}

There is also a two-parameter version of handle(), allowing you to pass both the body and an Authorizer instance:

@POST
@Path("plugins/install")
public Response installPlugin(Map<String, Object> jsonBody) {
    return handle(new CapabilityAuthorizer("SomeCapability"), () -> {
        // your business logic here
    });
}

Here is what handle() does:

  1. Starts a performance Meter immediately, unless you’ve disabled this behavior.
  2. If you have chosen to capture logs, starts the log capture process.
  3. Checks the user’s authorization, with many options. Note that IIQ will already have checked its own authorization annotations, such as @SystemAdmin by here. If you want to implement your own authentication, you will want to use IIQ’s @Deferred.
  4. Invokes your method body.
  5. Processes the return value, ensuring that it’s an allowed type, and translates it to JSON. To change which return types are allowed, use the @ResponsesAllowed annotation. You must return a non-null value unless you annotate your method with @NoReturnValue.
  6. If your code or handle throws an exception at any point, derives an HTTP error code and a standardized response body, which is returned to the client. This allows you to throw exceptions from your method body without worrying about Responses. Errors will also be logged to IIQ’s syslog.

Return types

The following return types are automatically managed by handle:

  • Response
  • ErrorResponse (see below)
  • Strings
  • Lists
  • Maps
  • Date
  • AbstractXmlObject
  • Any object extending IIQCommon’s RestObject
  • Any Exception object (which will be handled as though you threw an exception)

All other objects will result in an exception unless you annotate your class or method with @ResponsesAllowed. This prevents API developers from accidentally returning unexpected objects, which can potentially fail in the future or unexpectedly reveal system state.

Translation to JSON

Certain objects have special JSON translations that handle will automatically apply.

Dates will be translated to an object of type ExpandedDate. This contains a millisecond timestamp, as well as an ISO date string and time zone information about the date.

IIQ objects extending AbstractXmlObject will be translated to a JSON object containing the String xml, as well as the type, id, and name.

Error handling

Ordinarily, if you encounter an error in your method body, just throw an exception. It will be automatically translated into the appropriate type of response by handle. The default mapping is:

  • 400 Bad Request: IllegalArgumentException
  • 403 Forbidden: UnauthorizedAccessException, SecurityException
  • 404 Not Found: NotFoundException, ObjectNotFoundException
  • 500 Internal Server Error: Anything else

The response body will contain the following JSON fields:

  • exception: The exception class name
  • message: The exception message
  • quickKey: The syslog key corresponding to the error
  • logs: If logs were captured, the logs available

If the exception has a parent, the following are also added:

  • parentException: The class name of the parent exception
  • parentMessage: The message associated with the parent exception

Customizing error handling

You can override getExceptionMapping in your class if you would like to customize this exception response body.

You can override handleException in your class if you would like to customize the handling of “500” error response types. The exceptions handled as 400, 403, or 404 errors cannot be overridden.

If you want to do something totally custom (which I don’t recommend), you can build and return your own Response object from handle(), and it will be returned to the client as-is.

You can also return an instance of ErrorResponse, an IIQCommon class, which will contain the response code and response object. The response body will be validated and handled like a regular non-error response, but the HTTP response code will be set to your designated value.

Authorization

You have a variety of options for authorization, both declarative (via annotation) or programmatic. In general, you will want IIQ’s @Deferred annotation on your class or method to allow your resource, rather than IIQ’s default behavior, to handle the authorization.

IIQCommon supplies a custom annotation, @AuthorizedBy, which has optional arguments to authorize users by capability, right, population, rule, or attribute. You can combine these with @AuthorizeAll or @AuthorizeAny (but at this time, these cannot be nested). Place these at either the class level or the method level.

If you invoke handle() with an IIQ Authorizer, it will be invoked.

If you want the same authorization for every method, you have two options (and you can do both, if you want to, for some reason):

  • You can provide a PluginAuthorizationCheck object (usually in your constructor).
  • You can have your Resource class itself implement either Authorizer or PluginAuthorizationCheck.

Finally, if you want to use the ThingAccessUtils from IIQCommon, you can call checkThingAccess in your method body, providing the access criteria.

In each case, handle will throw an UnauthorizedAccessException if the authorization fails, which it will then translate into a 403 Forbidden response.

Validation

To enforce a consistent structure for validation, BaseCommonPluginResource exposes the following utility methods:

protected final void validate(PluginValidationCheck check) throws IllegalArgumentException;
protected final void validate(String failureMessage, PluginValidationCheck check) throws IllegalArgumentException;

You can use these by passing a lambda to the validate() method, which will throw an IllegalArgumentException (automatically translated to a 400 error) if the validation logic returns false or throws an exception. Boolean validations become one-liners.

public Response yourApiMethod(Map<String, Object> jsonBody) {
  return handle(() -> {
    // Returns a 400 Bad Request if the requiredValue key is not a String value
    validate(() -> !(jsonBody.get("requiredValue") instanceof String));
  });
}

Configuration

If you write a sufficiently complex plugin, you will need configuration. Even if you don’t distribute your plugin to others, you will encounter differences between various IIQ environments. IIQ offers “plugin settings”, which it stores on the Plugin object in the database. You can access and modify these via the usual Plugins UI.

The IIQ plugin settings interface for one of IID’s plugins

However, these settings are ephemeral. If you un-install and then re-install a plugin, which you may need to do for debugging, all of the settings return to their default values. To ensure that settings are retained across plugin installations, we typically use a Configuration object, which can be edited by the customer as needed. With that, though, there’s a new type of boilerplate: loading the Configuration object by name, reading a value from it, and casting it to the appropriate type.

BaseCommonPluginResource presents two options to make this straightforward.

Automatic configuration loading: First, you must override getConfigurationName to return the name of your Configuration object. (The default is “Plugin Configuration (your plugin name)“, which you likely don’t want.) Second, you may use any of the getConfigurationXXX methods (such as getConfigurationString) to read a value from the plugin configuration by key. Settings will be preferentially read from the named Configuration, but will fall back to the UI-based plugin settings.

Manual configuration loading: You may use the getConfiguration(String) method in your plugin code, providing the name of either a Configuration or Custom object (checked in that order). If one is found, its values are returned as an Attributes, abstracting away the need to know whether it was one object type or another. This method, notably, never returns null.