001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2015-2016 ForgeRock AS. 015 */ 016package org.forgerock.audit.json; 017 018import static org.forgerock.json.JsonValue.field; 019import static org.forgerock.json.JsonValue.json; 020import static org.forgerock.json.JsonValue.object; 021 022import java.io.IOException; 023import java.io.InputStream; 024import java.util.LinkedHashMap; 025import java.util.Map; 026 027import org.forgerock.audit.AuditException; 028import org.forgerock.audit.AuditServiceBuilder; 029import org.forgerock.audit.AuditServiceConfiguration; 030import org.forgerock.audit.events.handlers.AuditEventHandler; 031import org.forgerock.audit.events.handlers.EventHandlerConfiguration; 032import org.forgerock.audit.util.JsonValueUtils; 033import org.forgerock.json.JsonValue; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037import com.fasterxml.jackson.annotation.JsonPropertyDescription; 038import com.fasterxml.jackson.databind.AnnotationIntrospector; 039import com.fasterxml.jackson.databind.ObjectMapper; 040import com.fasterxml.jackson.databind.introspect.Annotated; 041import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; 042import com.fasterxml.jackson.module.jsonSchema.JsonSchema; 043import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper; 044 045/** 046 * Utility class to facilitate creation and configuration of audit service and audit event handlers 047 * through JSON. 048 */ 049public final class AuditJsonConfig { 050 private static final Logger LOGGER = LoggerFactory.getLogger(AuditJsonConfig.class); 051 052 /** Field containing the name of an event handler. */ 053 private static final String NAME_FIELD = "name"; 054 /** Field containing the implementation class of an event handler. */ 055 private static final String CLASS_FIELD = "class"; 056 /** Field containing the configuration of an event handler. */ 057 private static final String CONFIG_FIELD = "config"; 058 /** Field containing events topics to process for an event handler. */ 059 private static final String EVENTS_FIELD = "events"; 060 061 /** The mapper from JSON structure to Java object. */ 062 private static final ObjectMapper MAPPER = new ObjectMapper(); 063 064 private static final AnnotationIntrospector DEFAULT_ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector(); 065 private static final AnnotationIntrospector HELP_APPENDER_ANNOTATION_INTROSPECTOR = 066 new HelpAppenderAnnotationIntrospector(); 067 068 private AuditJsonConfig() { 069 // prevent instantiation of the class 070 } 071 072 /** 073 * Returns a JSON value from the provided input stream. 074 * 075 * @param input 076 * Input stream containing an arbitrary JSON structure. 077 * @return the JSON value corresponding to the JSON structure 078 * @throws AuditException 079 * If an error occurs. 080 */ 081 public static JsonValue getJson(InputStream input) throws AuditException { 082 if (input == null) { 083 throw new AuditException("Input stream is null"); 084 } 085 try { 086 Object val = MAPPER.readValue(input, LinkedHashMap.class); 087 return new JsonValue(val); 088 } catch (IOException e) { 089 throw new AuditException(String.format("Unable to retrieve json value from json input stream"), e); 090 } 091 } 092 093 /** 094 * Returns the audit service configuration from the provided input stream. 095 * 096 * @param input 097 * Input stream containing JSON configuration of the audit service. 098 * @return the configuration object 099 * @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}