View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.audit;
18  
19  import java.lang.annotation.Annotation;
20  import java.lang.reflect.InvocationHandler;
21  import java.lang.reflect.Method;
22  import java.lang.reflect.Proxy;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.concurrent.ConcurrentHashMap;
28  import java.util.concurrent.ConcurrentMap;
29  
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.logging.log4j.EventLogger;
32  import org.apache.logging.log4j.Level;
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.Logger;
35  import org.apache.logging.log4j.Marker;
36  import org.apache.logging.log4j.MarkerManager;
37  import org.apache.logging.log4j.ThreadContext;
38  import org.apache.logging.log4j.audit.annotation.Constraint;
39  import org.apache.logging.log4j.audit.annotation.Constraints;
40  import org.apache.logging.log4j.audit.annotation.MaxLength;
41  import org.apache.logging.log4j.audit.annotation.RequestContext;
42  import org.apache.logging.log4j.audit.annotation.RequestContextConstraints;
43  import org.apache.logging.log4j.audit.annotation.Required;
44  import org.apache.logging.log4j.audit.exception.AuditException;
45  import org.apache.logging.log4j.audit.util.NamingUtils;
46  import org.apache.logging.log4j.catalog.api.exception.ConstraintValidationException;
47  import org.apache.logging.log4j.catalog.api.plugins.ConstraintPlugins;
48  import org.apache.logging.log4j.message.StructuredDataMessage;
49  import org.apache.logging.log4j.spi.ExtendedLogger;
50  
51  import static org.apache.logging.log4j.catalog.api.util.StringUtils.appendNewline;
52  
53  /**
54   * Handles logging generated Events. Every Event extends the AuditProxy, which handles construction of the
55   * Event and logging of the Event.
56   */
57  public class LogEventFactory {
58  
59      private static final Logger logger = LogManager.getLogger(LogEventFactory.class);
60      private static final String NAME = "AuditLogger";
61      private static final String FQCN = LogEventFactory.class.getName();
62      private static Marker EVENT_MARKER = MarkerManager.getMarker("Audit").addParents(EventLogger.EVENT_MARKER);
63      private static final ExtendedLogger LOGGER = LogManager.getContext(false).getLogger(NAME);
64      private static final int DEFAULT_MAX_LENGTH = 32;
65  
66      private static final AuditExceptionHandler DEFAULT_HANDLER = (message, ex) -> {
67          throw new AuditException("Error logging event " + message.getId().getName(), ex);
68      };
69  
70      private static final AuditExceptionHandler NOOP_EXCEPTION_HANDLER = (message, ex) -> {
71      };
72  
73      private static AuditExceptionHandler defaultExceptionHandler = DEFAULT_HANDLER;
74  
75      private static ConcurrentMap<Class<?>, List<Property>> classMap = new ConcurrentHashMap<>();
76  
77      private static ConstraintPlugins constraintPlugins = ConstraintPlugins.getInstance();
78  
79      public static void setDefaultHandler(AuditExceptionHandler exceptionHandler) {
80          defaultExceptionHandler = (exceptionHandler == null) ? NOOP_EXCEPTION_HANDLER : exceptionHandler;
81      }
82  
83      /**
84       * Constructs an Event object from its interface.
85       * @param intrface The Event interface.
86       * @param <T> The Event type.
87       * @return Returns an instance of the Event.
88       */
89      @SuppressWarnings("unchecked")
90  	public static <T> T getEvent(Class<T> intrface) {
91  
92  		Class<?>[] interfaces = new Class<?>[] { intrface };
93  
94          String eventId = NamingUtils.lowerFirst(intrface.getSimpleName());
95          int msgLength = intrface.getAnnotation(MaxLength.class).value();
96          AuditMessage msg = new AuditMessage(eventId, msgLength);
97  		AuditEvent audit = (AuditEvent) Proxy.newProxyInstance(intrface
98  				.getClassLoader(), interfaces, new AuditProxy(msg, intrface));
99  
100 		return (T) audit;
101 	}
102 
103     /**
104      *
105      * This method is used to construct and AuditMessage from a set of properties and the Event interface
106      * that represents the event being audited using the default error handler.
107      * @param intrface The Event interface.
108      * @param properties The properties to be included in the event.
109      */
110     public static void logEvent(Class<?> intrface, Map<String, String> properties) {
111 	    logEvent(intrface, properties, DEFAULT_HANDLER);
112     }
113 
114     /**
115      * This method is used to construct and AuditMessage from a set of properties and the Event interface
116      * that represents the event being audited.
117      * @param intrface The Event interface.
118      * @param properties The properties to be included in the event.
119      * @param handler Class that gets control when an exception occurs logging the event.
120      */
121     public static void logEvent(Class<?> intrface, Map<String, String> properties, AuditExceptionHandler handler) {
122         StringBuilder errors = new StringBuilder();
123         validateContextConstraints(intrface, errors);
124 
125         String eventId = NamingUtils.lowerFirst(intrface.getSimpleName());
126         int maxLength = intrface.getAnnotation(MaxLength.class).value();
127         AuditMessage msg = new AuditMessage(eventId, maxLength);
128         List<Property> props = getProperties(intrface);
129         Map<String, Property> propertyMap = new HashMap<>();
130 
131         for (Property property : props ) {
132             propertyMap.put(property.name, property);
133             if (property.isRequired && !properties.containsKey(property.name)) {
134                 if (errors.length() > 0) {
135                     errors.append("\n");
136                 }
137                 errors.append("Required attribute ").append(property.name).append(" is missing from ").append(eventId);
138             }
139             if (properties.containsKey(property.name)) {
140                 validateConstraints(false, property.constraints, property.name, properties, errors);
141             }
142         }
143 
144         for (Map.Entry<String, String> entry : properties.entrySet()) {
145             if (!propertyMap.containsKey(entry.getKey())) {
146                 if (errors.length() > 0) {
147                     errors.append("Attribute ").append(entry.getKey()).append(" is not defined for ").append(eventId);
148                 }
149             }
150         }
151 
152         if (errors.length() > 0) {
153             throw new ConstraintValidationException(errors.toString());
154         }
155         for (Map.Entry<String, String> entry : properties.entrySet()) {
156             msg.put(entry.getKey(), entry.getValue());
157         }
158         logEvent(msg, handler);
159     }
160 
161     /**
162      * Used to Log the actual AuditMessage.
163      * @param msg The AuditMessage.
164      * @param handler Class that gets control when an exception occurs logging the event.
165      */
166     public static void logEvent(AuditMessage msg, AuditExceptionHandler handler) {
167         try {
168             LOGGER.logIfEnabled(FQCN, Level.OFF, EVENT_MARKER, msg, null);
169         } catch (Throwable ex) {
170             if (handler == null) {
171                 handler = defaultExceptionHandler;
172             }
173             handler.handleException(msg, ex);
174         }
175     }
176 
177     public static List<String> getPropertyNames(String className) {
178         Class<?> intrface = getClass(className);
179         List<String> names;
180         if (intrface != null) {
181             List<Property> props = getProperties(intrface);
182             names = new ArrayList<>(props.size());
183             for (Property prop : props) {
184                 names.add(prop.name);
185             }
186         } else {
187             names = new ArrayList<>();
188         }
189         return names;
190     }
191 
192     private static List<Property> getProperties(Class<?> intrface) {
193         List<Property> props = classMap.get(intrface);
194         if (props != null) {
195             return props;
196         }
197         props = new ArrayList<>();
198         Method[] methods = intrface.getMethods();
199         boolean isCompletionStatus = false;
200         for (Method method : methods) {
201             if (method.getName().startsWith("set") && !method.getName().equals("setAuditExceptionHandler")) {
202                 if (method.getName().equals("setCompletionStatus")) {
203                     isCompletionStatus = true;
204                 }
205                 String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
206                 Annotation[] annotations = method.getDeclaredAnnotations();
207                 List<Constraint> constraints = new ArrayList<>();
208                 boolean isRequired = false;
209                 for (Annotation annotation : annotations) {
210                     if (annotation instanceof Constraint) {
211                         constraints.add((Constraint) annotation);
212                     }
213                     if (annotation instanceof Required) {
214                         isRequired = true;
215                     }
216                 }
217                 props.add(new Property(name, isRequired, constraints));
218             }
219         }
220         if (!isCompletionStatus) {
221             props.add(new Property("completionStatus", false, new ArrayList<>()));
222         }
223 
224         classMap.putIfAbsent(intrface, props);
225         return classMap.get(intrface);
226     }
227 
228     private static Class<?> getClass(String className) {
229         try {
230             Class<?> intrface = Class.forName(className);
231             if (AuditEvent.class.isAssignableFrom(intrface)) {
232                 return intrface;
233             }
234             logger.error(className + " is not an AuditEvent");
235         } catch (ClassNotFoundException cnfe) {
236             logger.error("Unable to locate class {}", className);
237         }
238         return null;
239     }
240 
241 	private static class AuditProxy implements InvocationHandler {
242 
243 		private final AuditMessage msg;
244 		private final Class<?> intrface;
245         private AuditExceptionHandler auditExceptionHandler = DEFAULT_HANDLER;
246 
247 		AuditProxy(AuditMessage msg, Class<?> intrface) {
248 			this.msg = msg;
249 			this.intrface = intrface;
250 		}
251 
252         public AuditMessage getMessage() {
253             return msg;
254         }
255 
256 		@Override
257         @SuppressWarnings("unchecked")
258 		public Object invoke(Object o, Method method, Object[] objects)
259 				throws Throwable {
260 			if (method.getName().equals("logEvent")) {
261 
262 				StringBuilder errors = new StringBuilder();
263 				validateContextConstraints(intrface, errors);
264 
265                 StringBuilder missing = new StringBuilder();
266 				Method[] methods = intrface.getMethods();
267 
268 				for (Method _method : methods) {
269 					String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(_method.getName()));
270 
271 					Annotation[] annotations = _method.getDeclaredAnnotations();
272 					for (Annotation annotation : annotations) {
273                         if (annotation instanceof Required && msg.get(name) == null) {
274                             if (missing.length() > 0) {
275                                 missing.append(", ");
276                             }
277                             missing.append(name);
278                         }
279 					}
280 				}
281 				if (errors.length() > 0) {
282 				    if (missing.length() > 0) {
283 				        errors.append("\n");
284 				        errors.append("Required attributes are missing: ");
285 				        errors.append(missing.toString());
286                     }
287                 } else if (missing.length() > 0) {
288                     errors.append("Required attributes are missing: ");
289 				    errors = missing;
290                 }
291 
292 				if (errors.length() > 0) {
293 					throw new ConstraintValidationException("Event " + msg.getId().getName() +
294 							" has errors :\n" + errors.toString());
295 				}
296 
297                 logEvent(msg, auditExceptionHandler);
298 			}
299             if (method.getName().equals("setCompletionStatus")) {
300                 String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
301                 if (objects == null || objects[0] == null) {
302                     throw new IllegalArgumentException("Missing completion status");
303                 }
304                 msg.put(name, objects[0].toString());
305             }
306             if (method.getName().equals("setAuditExceptionHandler")) {
307 			    if (objects == null || objects[0] == null) {
308                     auditExceptionHandler = NOOP_EXCEPTION_HANDLER;
309                 } else if (objects[0] instanceof AuditExceptionHandler) {
310 			        auditExceptionHandler = (AuditExceptionHandler) objects[0];
311                 } else {
312 			        throw new IllegalArgumentException(objects[0] + " is not an " + AuditExceptionHandler.class.getName());
313                 }
314             }
315 			if (method.getName().startsWith("set")) {
316 				String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
317 				if (objects == null || objects[0] == null) {
318 				    throw new IllegalArgumentException("No value to be set for " + name);
319                 }
320 
321                 Annotation[] annotations = method.getDeclaredAnnotations();
322 				Class<?> returnType = method.getReturnType();
323 				StringBuilder errors = new StringBuilder();
324                 for (Annotation annotation : annotations) {
325 
326                     if (annotation instanceof Constraints) {
327                         Constraints constraints = (Constraints) annotation;
328                         validateConstraints(false, constraints.value(), name, objects[0].toString(),
329                                 errors);
330                     } else if (annotation instanceof Constraint) {
331                         Constraint constraint = (Constraint) annotation;
332                         constraintPlugins.validateConstraint(false, constraint.constraintType(),
333                                 name, objects[0].toString(), constraint.constraintValue(), errors);
334                     }
335                 }
336                 if (errors.length() > 0) {
337                     throw new ConstraintValidationException(errors.toString());
338                 }
339                 String result;
340                 if (objects[0] instanceof List) {
341                     result = StringUtils.join(objects, ", ");
342                 } else if (objects[0] instanceof Map) {
343                     StructuredDataMessage extra = new StructuredDataMessage(name, null, null);
344                     extra.putAll((Map)objects[0]);
345                     msg.addContent(name, extra);
346                     return null;
347                 } else {
348                     result = objects[0].toString();
349                 }
350 
351 				msg.put(name, result);
352 			}
353 
354 			return null;
355 		}
356     }
357 
358     private static void validateConstraints(boolean isRequestContext, Constraint[] constraints, String name,
359                                             Map<String, String> properties, StringBuilder errors) {
360         String value = isRequestContext ? ThreadContext.get(name) : properties.get(name);
361         validateConstraints(isRequestContext, constraints, name, value, errors);
362     }
363 
364     private static void validateConstraints(boolean isRequestContext, Constraint[] constraints, String name,
365                                             String value, StringBuilder errors) {
366         for (Constraint constraint : constraints) {
367             constraintPlugins.validateConstraint(isRequestContext, constraint.constraintType(), name, value,
368                     constraint.constraintValue(), errors);
369         }
370     }
371 
372     private static void validateContextConstraints(Class<?> intrface, StringBuilder buffer) {
373         RequestContextConstraints reqCtxConstraints = intrface.getAnnotation(RequestContextConstraints.class);
374         if (reqCtxConstraints != null) {
375             for (RequestContext ctx : reqCtxConstraints.value()) {
376                 validateContextConstraint(ctx, buffer);
377             }
378         } else {
379             RequestContext ctx = intrface.getAnnotation(RequestContext.class);
380             validateContextConstraint(ctx, buffer);
381         }
382     }
383 
384     private static void validateContextConstraint(RequestContext constraint, StringBuilder errors) {
385         String value = ThreadContext.get(constraint.key());
386         if (value != null) {
387             validateConstraints(true, constraint.constraints(), constraint.key(), value, errors);
388         } else if (constraint.required()) {
389             appendNewline(errors);
390             errors.append("ThreadContext does not contain required key ").append(constraint.key());
391         }
392     }
393 
394     private static boolean isBlank(String value) {
395         return value != null && value.length() > 0;
396     }
397 
398     private static class Property {
399         private final String name;
400         private final boolean isRequired;
401         private final Constraint[] constraints;
402 
403         public Property(String name, boolean isRequired, List<Constraint> constraints) {
404             this.name = name;
405             this.constraints = constraints.toArray(new Constraint[constraints.size()]);
406             this.isRequired = isRequired;
407         }
408     }
409 
410 }