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