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 com.fasterxml.jackson.annotation.JsonPropertyDescription; 023import com.fasterxml.jackson.databind.AnnotationIntrospector; 024import com.fasterxml.jackson.databind.ObjectMapper; 025import com.fasterxml.jackson.databind.introspect.Annotated; 026import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; 027import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; 028import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.SchemaFactoryWrapper; 029import java.io.IOException; 030import java.io.InputStream; 031import java.util.LinkedHashMap; 032import java.util.Map; 033import org.forgerock.audit.AuditException; 034import org.forgerock.audit.AuditServiceBuilder; 035import org.forgerock.audit.AuditServiceConfiguration; 036import org.forgerock.audit.events.handlers.AuditEventHandler; 037import org.forgerock.audit.events.handlers.EventHandlerConfiguration; 038import org.forgerock.audit.util.JsonValueUtils; 039import org.forgerock.json.JsonValue; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043/** 044 * Utility class to facilitate creation and configuration of audit service and audit event handlers 045 * through JSON. 046 */ 047public final class AuditJsonConfig { 048 private static final Logger LOGGER = LoggerFactory.getLogger(AuditJsonConfig.class); 049 050 /** Field containing the name of an event handler. */ 051 private static final String NAME_FIELD = "name"; 052 /** Field containing the implementation class of an event handler. */ 053 private static final String CLASS_FIELD = "class"; 054 /** Field containing the configuration of an event handler. */ 055 private static final String CONFIG_FIELD = "config"; 056 /** Field containing events topics to process for an event handler. */ 057 private static final String EVENTS_FIELD = "events"; 058 059 /** The mapper from JSON structure to Java object. */ 060 private static final ObjectMapper MAPPER = new ObjectMapper(); 061 062 private static final AnnotationIntrospector DEFAULT_ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector(); 063 private static final AnnotationIntrospector HELP_APPENDER_ANNOTATION_INTROSPECTOR = 064 new HelpAppenderAnnotationIntrospector(); 065 066 private AuditJsonConfig() { 067 // prevent instantiation of the class 068 } 069 070 /** 071 * Returns a JSON value from the provided input stream. 072 * 073 * @param input 074 * Input stream containing an arbitrary JSON structure. 075 * @return the JSON value corresponding to the JSON structure 076 * @throws AuditException 077 * If an error occurs. 078 */ 079 public static JsonValue getJson(InputStream input) throws AuditException { 080 if (input == null) { 081 throw new AuditException("Input stream is null"); 082 } 083 try { 084 Object val = MAPPER.readValue(input, LinkedHashMap.class); 085 return new JsonValue(val); 086 } catch (IOException e) { 087 throw new AuditException(String.format("Unable to retrieve json value from json input stream"), e); 088 } 089 } 090 091 /** 092 * Returns the audit service configuration from the provided input stream. 093 * 094 * @param input 095 * Input stream containing JSON configuration of the audit service. 096 * @return the configuration object 097 * @throws AuditException 098 * If any error occurs. 099 */ 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}