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}