View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2015-2016 ForgeRock AS.
15   */
16  package org.forgerock.audit.json;
17  
18  import static org.forgerock.json.JsonValue.field;
19  import static org.forgerock.json.JsonValue.json;
20  import static org.forgerock.json.JsonValue.object;
21  
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.util.LinkedHashMap;
25  import java.util.Map;
26  
27  import org.forgerock.audit.AuditException;
28  import org.forgerock.audit.AuditServiceBuilder;
29  import org.forgerock.audit.AuditServiceConfiguration;
30  import org.forgerock.audit.events.handlers.AuditEventHandler;
31  import org.forgerock.audit.events.handlers.EventHandlerConfiguration;
32  import org.forgerock.audit.util.JsonValueUtils;
33  import org.forgerock.json.JsonValue;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import com.fasterxml.jackson.annotation.JsonPropertyDescription;
38  import com.fasterxml.jackson.databind.AnnotationIntrospector;
39  import com.fasterxml.jackson.databind.ObjectMapper;
40  import com.fasterxml.jackson.databind.introspect.Annotated;
41  import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
42  import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
43  import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper;
44  
45  /**
46   * Utility class to facilitate creation and configuration of audit service and audit event handlers
47   * through JSON.
48   */
49  public final class AuditJsonConfig {
50      private static final Logger LOGGER = LoggerFactory.getLogger(AuditJsonConfig.class);
51  
52      /** Field containing the name of an event handler. */
53      private static final String NAME_FIELD = "name";
54      /** Field containing the implementation class of an event handler. */
55      private static final String CLASS_FIELD = "class";
56      /** Field containing the configuration of an event handler. */
57      private static final String CONFIG_FIELD = "config";
58      /** Field containing events topics to process for an event handler. */
59      private static final String EVENTS_FIELD = "events";
60  
61      /** The mapper from JSON structure to Java object. */
62      private static final ObjectMapper MAPPER = new ObjectMapper();
63  
64      private static final AnnotationIntrospector DEFAULT_ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector();
65      private static final AnnotationIntrospector HELP_APPENDER_ANNOTATION_INTROSPECTOR =
66              new HelpAppenderAnnotationIntrospector();
67  
68      private AuditJsonConfig() {
69          // prevent instantiation of the class
70      }
71  
72      /**
73       * Returns a JSON value from the provided input stream.
74       *
75       * @param input
76       *          Input stream containing an arbitrary JSON structure.
77       * @return the JSON value corresponding to the JSON structure
78       * @throws AuditException
79       *          If an error occurs.
80       */
81      public static JsonValue getJson(InputStream input) throws AuditException {
82          if (input == null) {
83              throw new AuditException("Input stream is null");
84          }
85          try {
86              Object val = MAPPER.readValue(input, LinkedHashMap.class);
87              return new JsonValue(val);
88          } catch (IOException e) {
89              throw new AuditException(String.format("Unable to retrieve json value from json input stream"), e);
90          }
91      }
92  
93      /**
94       * Returns the audit service configuration from the provided input stream.
95       *
96       * @param input
97       *          Input stream containing JSON configuration of the audit service.
98       * @return the configuration object
99       * @throws AuditException
100      *          If any error occurs.
101      */
102     public static AuditServiceConfiguration parseAuditServiceConfiguration(InputStream input) throws AuditException {
103         try {
104             return MAPPER.readValue(input, AuditServiceConfiguration.class);
105         } catch (IOException e) {
106             throw new AuditException(String.format("Unable to retrieve class %s from json input stream",
107                     AuditServiceConfiguration.class), e);
108         }
109     }
110 
111     /**
112      * Returns the audit service configuration from the provided JSON string.
113      *
114      * @param json
115      *          JSON string representing the configuration of the audit service.
116      * @return the configuration object
117      * @throws AuditException
118      *          If any error occurs.
119      */
120     public static AuditServiceConfiguration parseAuditServiceConfiguration(String json) throws AuditException {
121         if (json == null) {
122             return new AuditServiceConfiguration();
123         }
124         try {
125             return MAPPER.readValue(json, AuditServiceConfiguration.class);
126         } catch (IOException e) {
127             throw new AuditException(String.format("Unable to retrieve class %s from json: %s",
128                     AuditServiceConfiguration.class, json), e);
129         }
130     }
131 
132     /**
133      * Returns the audit service configuration from the provided JSON value.
134      *
135      * @param json
136      *          JSON value representing the configuration of the audit service.
137      * @return the configuration object
138      * @throws AuditException
139      *          If any error occurs.
140      */
141     public static AuditServiceConfiguration parseAuditServiceConfiguration(JsonValue json) throws AuditException {
142         return parseAuditServiceConfiguration(JsonValueUtils.extractValueAsString(json, "/"));
143     }
144 
145     /**
146      * Configures and registers the audit event handler corresponding to the provided JSON configuration
147      * to the provided audit service.
148      *
149      * @param jsonConfig
150      *          The configuration of the audit event handler as JSON.
151      * @param auditServiceBuilder
152      *          The builder for the service the event handler will be registered to.
153      * @throws AuditException
154      *             If any error occurs during configuration or registration of the handler.
155      */
156     public static void registerHandlerToService(JsonValue jsonConfig, AuditServiceBuilder auditServiceBuilder)
157             throws AuditException {
158         registerHandlerToService(jsonConfig, auditServiceBuilder, auditServiceBuilder.getClass().getClassLoader());
159     }
160 
161     /**
162      * Configures and registers the audit event handler corresponding to the provided JSON configuration
163      * to the provided audit service, using a specific class loader.
164      *
165      * @param jsonConfig
166      *          The configuration of the audit event handler as JSON.
167      * @param auditServiceBuilder
168      *          The builder for the service the event handler will be registered to.
169      * @param classLoader
170      *          The class loader to use to load the handler and its configuration class.
171      * @throws AuditException
172      *             If any error occurs during configuration or registration of the handler.
173      */
174     public static void registerHandlerToService(JsonValue jsonConfig,
175             AuditServiceBuilder auditServiceBuilder, ClassLoader classLoader) throws AuditException {
176         String name = getHandlerName(jsonConfig);
177         Class<? extends AuditEventHandler> handlerClass = getAuditEventHandlerClass(name, jsonConfig, classLoader);
178         Class<? extends EventHandlerConfiguration> configClass =
179                 getAuditEventHandlerConfigurationClass(name, handlerClass, classLoader);
180         EventHandlerConfiguration configuration = parseAuditEventHandlerConfiguration(configClass, jsonConfig);
181         auditServiceBuilder.withAuditEventHandler(handlerClass, configuration);
182     }
183 
184     /**
185      * Returns the name of the event handler corresponding to provided JSON configuration.
186      * <p>
187      * The JSON configuration is expected to contains a "name" field identifying the
188      * event handler, e.g.
189      * <pre>
190      *  "name" : "passthrough"
191      * </pre>
192      *
193      * @param jsonConfig
194      *          The JSON configuration of the event handler.
195      * @return the name of the event handler
196      * @throws AuditException
197      *          If an error occurs.
198      */
199     private static String getHandlerName(JsonValue jsonConfig) throws AuditException {
200         String name = jsonConfig.get(CONFIG_FIELD).get(NAME_FIELD).asString();
201         if (name == null) {
202             throw new AuditException(String.format("No name is defined for the provided audit handler. "
203                     + "You must define a 'name' property in the configuration."));
204         }
205         return name;
206     }
207 
208     /**
209      * Creates an audit event handler factory from the provided JSON configuration.
210      * <p>
211      * The JSON configuration is expected to contains a "class" property which provides
212      * the class name for the handler factory to instantiate.
213      *
214      * @param jsonConfig
215      *          The configuration of the audit event handler as JSON.
216      * @param classLoader
217      *          The class loader to use to load the handler and its configuration class.
218      * @return the fully configured audit event handler
219      * @throws AuditException
220      *             If any error occurs during configuration or registration of the handler.
221      */
222     @SuppressWarnings("unchecked") // Class.forName calls
223     private static Class<? extends AuditEventHandler> getAuditEventHandlerClass(
224             String handlerName, JsonValue jsonConfig, ClassLoader classLoader) throws AuditException {
225         // TODO: class name should not be provided in customer configuration
226         // but through a commons module/service context
227         String className = jsonConfig.get(CLASS_FIELD).asString();
228         if (className == null) {
229             String errorMessage = String.format("No class is defined for the audit handler %s. "
230                     + "You must define a 'class' property in the configuration.", handlerName);
231             throw new AuditException(errorMessage);
232         }
233         try {
234             return (Class<? extends AuditEventHandler>) Class.forName(className, true, classLoader);
235         } catch (ClassNotFoundException e) {
236             String errorMessage = String.format("Invalid class is defined for the audit handler %s.", handlerName);
237             throw new AuditException(errorMessage, e);
238         }
239     }
240 
241     @SuppressWarnings("unchecked") // Class.forName calls
242     private static Class<? extends EventHandlerConfiguration> getAuditEventHandlerConfigurationClass(
243             String handlerName, Class<? extends AuditEventHandler> handlerClass, ClassLoader classLoader)
244             throws AuditException {
245         String className = handlerClass.getName() + "Configuration";
246         try {
247             return (Class<? extends EventHandlerConfiguration>) Class.forName(className, true, classLoader);
248         } catch (ClassNotFoundException e) {
249             String errorMessage = String.format("Unable to locate configuration class %s for the audit handler %s.",
250                     className, handlerName);
251             throw new AuditException(errorMessage, e);
252         }
253     }
254 
255     /**
256      * Returns the audit event handler configuration from the provided JSON string.
257      *
258      * @param <C>
259      *          The type of the configuration bean for the event handler.
260      * @param jsonConfig
261      *          The configuration of the audit event handler as JSON.
262      * @param clazz The class for type {@code <C>}.
263      * @return the fully configured audit event handler
264      * @throws AuditException
265      *             If any error occurs while instantiating the configuration from JSON.
266      */
267     public static <C extends EventHandlerConfiguration> C parseAuditEventHandlerConfiguration(
268             Class<C> clazz, JsonValue jsonConfig) throws AuditException {
269         C configuration = null;
270         JsonValue conf = jsonConfig.get(CONFIG_FIELD);
271         if (conf != null) {
272             configuration = MAPPER.convertValue(conf.getObject(), clazz);
273         }
274         return configuration;
275     }
276 
277     /**
278      * Gets the configuration schema for an audit event handler as json schema. The supplied json config must contain
279      * a field called class with the value of the audit event handler implementation class.
280      * @param className The class name to get the configuration for.
281      * @param classLoader The {@link ClassLoader} to use to load the event handler and event handler config class.
282      * @return The config schema as json schema.
283      * @throws AuditException If any error occurs parsing the config class for schema.
284      */
285     public static JsonValue getAuditEventHandlerConfigurationSchema(final String className,
286             final ClassLoader classLoader) throws AuditException {
287         final Class<? extends EventHandlerConfiguration> eventHandlerConfiguration =
288                 getAuditEventHandlerConfigurationClass(
289                         className,
290                         getAuditEventHandlerClass(
291                                 className,
292                                 json(object(field("class", className))),
293                                 classLoader),
294                         classLoader);
295         try {
296             MAPPER.setAnnotationIntrospector(HELP_APPENDER_ANNOTATION_INTROSPECTOR);
297             SchemaFactoryWrapper visitor = new SchemaFactoryWrapper();
298             MAPPER.acceptJsonFormatVisitor(MAPPER.constructType(eventHandlerConfiguration), visitor);
299             JsonSchema jsonSchema = visitor.finalSchema();
300             final JsonValue schema = json(MAPPER.readValue(MAPPER.writeValueAsString(jsonSchema), Map.class));
301             MAPPER.setAnnotationIntrospector(DEFAULT_ANNOTATION_INTROSPECTOR);
302             return schema;
303         } catch (IOException e) {
304             final String error = String.format("Unable to parse configuration class schema for configuration class %s",
305                     eventHandlerConfiguration.getName());
306             LOGGER.error(error, e);
307             throw new AuditException(error, e);
308         }
309     }
310 
311     /**
312      * Extends the default {@link JacksonAnnotationIntrospector} and overrides the {@link JsonPropertyDescription}
313      * annotation inorder to append ".help" to the description.
314      */
315     private static class HelpAppenderAnnotationIntrospector extends JacksonAnnotationIntrospector {
316         @Override
317         public String findPropertyDescription(Annotated ann) {
318             JsonPropertyDescription desc = _findAnnotation(ann, JsonPropertyDescription.class);
319             return (desc == null) ? null : desc.value().concat(".help");
320         }
321     }
322 
323 }