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 try { 083 return new JsonValue(MAPPER.readValue(input, LinkedHashMap.class)); 084 } catch (IOException e) { 085 throw new AuditException(String.format("Unable to retrieve json value from json input stream"), e); 086 } 087 } 088 089 /** 090 * Returns the audit service configuration from the provided input stream. 091 * 092 * @param input 093 * Input stream containing JSON configuration of the audit service. 094 * @return the configuration object 095 * @throws AuditException 096 * If any error occurs. 097 */ 098 public static AuditServiceConfiguration parseAuditServiceConfiguration(InputStream input) throws AuditException { 099 try { 100 return MAPPER.readValue(input, AuditServiceConfiguration.class); 101 } catch (IOException e) { 102 throw new AuditException(String.format("Unable to retrieve class %s from json input stream", 103 AuditServiceConfiguration.class), e); 104 } 105 } 106 107 /** 108 * Returns the audit service configuration from the provided JSON string. 109 * 110 * @param json 111 * JSON string representing the configuration of the audit service. 112 * @return the configuration object 113 * @throws AuditException 114 * If any error occurs. 115 */ 116 public static AuditServiceConfiguration parseAuditServiceConfiguration(String json) throws AuditException { 117 if (json == null) { 118 return new AuditServiceConfiguration(); 119 } 120 try { 121 return MAPPER.readValue(json, AuditServiceConfiguration.class); 122 } catch (IOException e) { 123 throw new AuditException(String.format("Unable to retrieve class %s from json: %s", 124 AuditServiceConfiguration.class, json), e); 125 } 126 } 127 128 /** 129 * Returns the audit service configuration from the provided JSON value. 130 * 131 * @param json 132 * JSON value representing the configuration of the audit service. 133 * @return the configuration object 134 * @throws AuditException 135 * If any error occurs. 136 */ 137 public static AuditServiceConfiguration parseAuditServiceConfiguration(JsonValue json) throws AuditException { 138 return parseAuditServiceConfiguration(JsonValueUtils.extractValueAsString(json, "/")); 139 } 140 141 /** 142 * Configures and registers the audit event handler corresponding to the provided JSON configuration 143 * to the provided audit service. 144 * 145 * @param jsonConfig 146 * The configuration of the audit event handler as JSON. 147 * @param auditServiceBuilder 148 * The builder for the service the event handler will be registered to. 149 * @throws AuditException 150 * If any error occurs during configuration or registration of the handler. 151 */ 152 public static void registerHandlerToService(JsonValue jsonConfig, AuditServiceBuilder auditServiceBuilder) 153 throws AuditException { 154 registerHandlerToService(jsonConfig, auditServiceBuilder, auditServiceBuilder.getClass().getClassLoader()); 155 } 156 157 /** 158 * Configures and registers the audit event handler corresponding to the provided JSON configuration 159 * to the provided audit service, using a specific class loader. 160 * 161 * @param jsonConfig 162 * The configuration of the audit event handler as JSON. 163 * @param auditServiceBuilder 164 * The builder for the service the event handler will be registered to. 165 * @param classLoader 166 * The class loader to use to load the handler and its configuration class. 167 * @throws AuditException 168 * If any error occurs during configuration or registration of the handler. 169 */ 170 public static void registerHandlerToService(JsonValue jsonConfig, 171 AuditServiceBuilder auditServiceBuilder, ClassLoader classLoader) throws AuditException { 172 String name = getHandlerName(jsonConfig); 173 Class<? extends AuditEventHandler> handlerClass = getAuditEventHandlerClass(name, jsonConfig, classLoader); 174 Class<? extends EventHandlerConfiguration> configClass = 175 getAuditEventHandlerConfigurationClass(name, handlerClass, classLoader); 176 EventHandlerConfiguration configuration = parseAuditEventHandlerConfiguration(configClass, jsonConfig); 177 auditServiceBuilder.withAuditEventHandler(handlerClass, configuration); 178 } 179 180 /** 181 * Returns the name of the event handler corresponding to provided JSON configuration. 182 * <p> 183 * The JSON configuration is expected to contains a "name" field identifying the 184 * event handler, e.g. 185 * <pre> 186 * "name" : "passthrough" 187 * </pre> 188 * 189 * @param jsonConfig 190 * The JSON configuration of the event handler. 191 * @return the name of the event handler 192 * @throws AuditException 193 * If an error occurs. 194 */ 195 private static String getHandlerName(JsonValue jsonConfig) throws AuditException { 196 String name = jsonConfig.get(CONFIG_FIELD).get(NAME_FIELD).asString(); 197 if (name == null) { 198 throw new AuditException(String.format("No name is defined for the provided audit handler. " 199 + "You must define a 'name' property in the configuration.")); 200 } 201 return name; 202 } 203 204 /** 205 * Creates an audit event handler factory from the provided JSON configuration. 206 * <p> 207 * The JSON configuration is expected to contains a "class" property which provides 208 * the class name for the handler factory to instantiate. 209 * 210 * @param jsonConfig 211 * The configuration of the audit event handler as JSON. 212 * @param classLoader 213 * The class loader to use to load the handler and its configuration class. 214 * @return the fully configured audit event handler 215 * @throws AuditException 216 * If any error occurs during configuration or registration of the handler. 217 */ 218 @SuppressWarnings("unchecked") // Class.forName calls 219 private static Class<? extends AuditEventHandler> getAuditEventHandlerClass( 220 String handlerName, JsonValue jsonConfig, ClassLoader classLoader) throws AuditException { 221 // TODO: class name should not be provided in customer configuration 222 // but through a commons module/service context 223 String className = jsonConfig.get(CLASS_FIELD).asString(); 224 if (className == null) { 225 String errorMessage = String.format("No class is defined for the audit handler %s. " 226 + "You must define a 'class' property in the configuration.", handlerName); 227 throw new AuditException(errorMessage); 228 } 229 try { 230 return (Class<? extends AuditEventHandler>) Class.forName(className, true, classLoader); 231 } catch (ClassNotFoundException e) { 232 String errorMessage = String.format("Invalid class is defined for the audit handler %s.", handlerName); 233 throw new AuditException(errorMessage, e); 234 } 235 } 236 237 @SuppressWarnings("unchecked") // Class.forName calls 238 private static Class<? extends EventHandlerConfiguration> getAuditEventHandlerConfigurationClass( 239 String handlerName, Class<? extends AuditEventHandler> handlerClass, ClassLoader classLoader) 240 throws AuditException { 241 String className = handlerClass.getName() + "Configuration"; 242 try { 243 return (Class<? extends EventHandlerConfiguration>) Class.forName(className, true, classLoader); 244 } catch (ClassNotFoundException e) { 245 String errorMessage = String.format("Unable to locate configuration class %s for the audit handler %s.", 246 className, handlerName); 247 throw new AuditException(errorMessage, e); 248 } 249 } 250 251 /** 252 * Returns the audit event handler configuration from the provided JSON string. 253 * 254 * @param <C> 255 * The type of the configuration bean for the event handler. 256 * @param jsonConfig 257 * The configuration of the audit event handler as JSON. 258 * @param clazz The class for type {@code <C>}. 259 * @return the fully configured audit event handler 260 * @throws AuditException 261 * If any error occurs while instantiating the configuration from JSON. 262 */ 263 public static <C extends EventHandlerConfiguration> C parseAuditEventHandlerConfiguration( 264 Class<C> clazz, JsonValue jsonConfig) throws AuditException { 265 C configuration = null; 266 JsonValue conf = jsonConfig.get(CONFIG_FIELD); 267 if (conf != null) { 268 configuration = MAPPER.convertValue(conf.getObject(), clazz); 269 } 270 return configuration; 271 } 272 273 /** 274 * Gets the configuration schema for an audit event handler as json schema. The supplied json config must contain 275 * a field called class with the value of the audit event handler implementation class. 276 * @param className The class name to get the configuration for. 277 * @param classLoader The {@link ClassLoader} to use to load the event handler and event handler config class. 278 * @return The config schema as json schema. 279 * @throws AuditException If any error occurs parsing the config class for schema. 280 */ 281 public static JsonValue getAuditEventHandlerConfigurationSchema(final String className, 282 final ClassLoader classLoader) throws AuditException { 283 final Class<? extends EventHandlerConfiguration> eventHandlerConfiguration = 284 getAuditEventHandlerConfigurationClass( 285 className, 286 getAuditEventHandlerClass( 287 className, 288 json(object(field("class", className))), 289 classLoader), 290 classLoader); 291 try { 292 MAPPER.setAnnotationIntrospector(HELP_APPENDER_ANNOTATION_INTROSPECTOR); 293 SchemaFactoryWrapper visitor = new SchemaFactoryWrapper(); 294 MAPPER.acceptJsonFormatVisitor(MAPPER.constructType(eventHandlerConfiguration), visitor); 295 JsonSchema jsonSchema = visitor.finalSchema(); 296 final JsonValue schema = json(MAPPER.readValue(MAPPER.writeValueAsString(jsonSchema), Map.class)); 297 MAPPER.setAnnotationIntrospector(DEFAULT_ANNOTATION_INTROSPECTOR); 298 return schema; 299 } catch (IOException e) { 300 final String error = String.format("Unable to parse configuration class schema for configuration class %s", 301 eventHandlerConfiguration.getName()); 302 LOGGER.error(error, e); 303 throw new AuditException(error, e); 304 } 305 } 306 307 /** 308 * Extends the default {@link JacksonAnnotationIntrospector} and overrides the {@link JsonPropertyDescription} 309 * annotation inorder to append ".help" to the description. 310 */ 311 private static class HelpAppenderAnnotationIntrospector extends JacksonAnnotationIntrospector { 312 @Override 313 public String findPropertyDescription(Annotated ann) { 314 JsonPropertyDescription desc = _findAnnotation(ann, JsonPropertyDescription.class); 315 return (desc == null) ? null : desc.value().concat(".help"); 316 } 317 } 318 319}