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.*;
24  import java.util.concurrent.ConcurrentHashMap;
25  import java.util.concurrent.ConcurrentMap;
26  
27  import org.apache.commons.lang3.StringUtils;
28  import org.apache.logging.log4j.LogManager;
29  import org.apache.logging.log4j.Logger;
30  import org.apache.logging.log4j.ThreadContext;
31  import org.apache.logging.log4j.audit.annotation.Constraint;
32  import org.apache.logging.log4j.audit.annotation.Constraints;
33  import org.apache.logging.log4j.audit.annotation.MaxLength;
34  import org.apache.logging.log4j.audit.annotation.RequestContext;
35  import org.apache.logging.log4j.audit.annotation.RequestContextConstraints;
36  import org.apache.logging.log4j.audit.annotation.Required;
37  import org.apache.logging.log4j.audit.exception.AuditException;
38  import org.apache.logging.log4j.audit.util.NamingUtils;
39  import org.apache.logging.log4j.audit.exception.ConstraintValidationException;
40  import org.apache.logging.log4j.catalog.api.plugins.ConstraintPlugins;
41  import org.apache.logging.log4j.message.StructuredDataMessage;
42  
43  import static org.apache.logging.log4j.catalog.api.util.StringUtils.appendNewline;
44  
45  /**
46   * Handles logging generated Events. Every Event extends the AuditProxy, which handles construction of the
47   * Event and logging of the Event.
48   */
49  public class LogEventFactory {
50  
51      private static final Logger logger = LogManager.getLogger(LogEventFactory.class);
52  
53      private static final AuditLogger AUDIT_LOGGER = new AuditLogger();
54  
55      private static final int DEFAULT_MAX_LENGTH = 32;
56  
57      private static final AuditExceptionHandler DEFAULT_HANDLER = (message, ex) -> {
58          throw new AuditException("Error logging event " + message.getId().getName(), ex);
59      };
60  
61      private static final AuditExceptionHandler NOOP_EXCEPTION_HANDLER = (message, ex) -> {
62      };
63  
64      private static AuditExceptionHandler defaultExceptionHandler = DEFAULT_HANDLER;
65  
66      private static ConcurrentMap<Class<?>, List<Property>> classMap = new ConcurrentHashMap<>();
67  
68      private static ConstraintPlugins constraintPlugins = ConstraintPlugins.getInstance();
69  
70      public static void setDefaultHandler(AuditExceptionHandler exceptionHandler) {
71          defaultExceptionHandler = (exceptionHandler == null) ? NOOP_EXCEPTION_HANDLER : exceptionHandler;
72      }
73  
74  	static void resetDefaultHandler() {
75  		defaultExceptionHandler = DEFAULT_HANDLER;
76  	}
77  
78      /**
79       * Constructs an Event object from its interface.
80       * @param intrface The Event interface.
81       * @param <T> The Event type.
82       * @return Returns an instance of the Event.
83       */
84      @SuppressWarnings("unchecked")
85  	public static <T extends AuditEvent> T getEvent(Class<T> intrface) {
86  
87  		Class<?>[] interfaces = new Class<?>[] { intrface };
88  
89  	    AuditMessage msg = buildAuditMessage(intrface);
90  	    AuditEvent audit = (AuditEvent) Proxy.newProxyInstance(intrface.getClassLoader(), interfaces,
91  			    new AuditProxy(msg, intrface, defaultExceptionHandler));
92  
93  		return (T) audit;
94  	}
95  
96  	private static <T> int getMaxLength(Class<T> intrface) {
97          MaxLength maxLength = intrface.getAnnotation(MaxLength.class);
98          return maxLength == null ? DEFAULT_MAX_LENGTH : maxLength.value();
99      }
100 
101 	private static AuditMessage buildAuditMessage(Class<?> intrface) {
102 		String eventId = NamingUtils.lowerFirst(intrface.getSimpleName());
103 		int msgLength = getMaxLength(intrface);
104 		return new AuditMessage(eventId, msgLength);
105 	}
106 
107     /**
108      *
109      * This method is used to construct and AuditMessage from a set of properties and the Event interface
110      * that represents the event being audited using the default error handler.
111      * @param intrface The Event interface.
112      * @param properties The properties to be included in the event.
113      */
114     public static void logEvent(Class<?> intrface, Map<String, String> properties) {
115 	    logEvent(intrface, properties, DEFAULT_HANDLER);
116     }
117 
118     /**
119      * This method is used to construct and AuditMessage from a set of properties and the Event interface
120      * that represents the event being audited.
121      * @param intrface The Event interface.
122      * @param properties The properties to be included in the event.
123      * @param handler Class that gets control when an exception occurs logging the event.
124      */
125     public static void logEvent(Class<?> intrface, Map<String, String> properties, AuditExceptionHandler handler) {
126 	    AuditMessage msg = buildAuditMessage(intrface);
127 
128 	    if (properties != null) {
129 		    for (Map.Entry<String, String> entry : properties.entrySet()) {
130 			    msg.put(entry.getKey(), entry.getValue());
131 		    }
132 	    }
133 
134 	    validateEvent(intrface, msg);
135 	    logEvent(msg, handler);
136     }
137 
138 	private static void validateEvent(Class<?> intrface, AuditMessage msg) {
139 		StringBuilder errors = new StringBuilder();
140 		validateContextConstraints(intrface, errors);
141 
142 		List<Property> props = getProperties(intrface);
143 		Map<String, Property> propertyMap = new HashMap<>();
144 
145 		for (Property property : props) {
146 		    propertyMap.put(property.name, property);
147 		    if (property.isRequired && !msg.containsKey(property.name)) {
148 		        if (errors.length() > 0) {
149 		            errors.append("\n");
150 		        }
151 		        errors.append("Required attribute ").append(property.name).append(" is missing from ").append(msg.getId().getName());
152 		    }
153 		    if (msg.containsKey(property.name)) {
154 		        validateConstraints(false, property.constraints, property.name, msg, errors);
155 		    }
156 		}
157 
158 		msg.forEach((key, value) -> {
159 			if (!propertyMap.containsKey(key)) {
160 				if (errors.length() > 0) {
161 					errors.append("Attribute ").append(key).append(" is not defined for ").append(msg.getId().getName());
162 				}
163 			}
164 		});
165 
166 		if (errors.length() > 0) {
167 		    throw new ConstraintValidationException(errors.toString());
168 		}
169 	}
170 
171     /**
172      * Used to Log the actual AuditMessage.
173      * @param msg The AuditMessage.
174      * @param handler Class that gets control when an exception occurs logging the event.
175      */
176     public static void logEvent(AuditMessage msg, AuditExceptionHandler handler) {
177 	    runMessageAction(() -> AUDIT_LOGGER.logEvent(msg), msg, handler);
178     }
179 
180 	private static void runMessageAction(Runnable action, AuditMessage msg, AuditExceptionHandler handler) {
181 		try {
182 			action.run();
183 		} catch (Throwable ex) {
184 		    if (handler == null) {
185 		        handler = defaultExceptionHandler;
186 		    }
187 		    handler.handleException(msg, ex);
188 		}
189 	}
190 
191 	public static List<String> getPropertyNames(String className) {
192         Class<?> intrface = getClass(className);
193         List<String> names;
194         if (intrface != null) {
195             List<Property> props = getProperties(intrface);
196             names = new ArrayList<>(props.size());
197             for (Property prop : props) {
198                 names.add(prop.name);
199             }
200         } else {
201             names = new ArrayList<>();
202         }
203         return names;
204     }
205 
206     private static List<Property> getProperties(Class<?> intrface) {
207         List<Property> props = classMap.get(intrface);
208         if (props != null) {
209             return props;
210         }
211         props = new ArrayList<>();
212         Method[] methods = intrface.getMethods();
213         boolean isCompletionStatus = false;
214         for (Method method : methods) {
215             if (method.getName().startsWith("set") && !method.getName().equals("setAuditExceptionHandler")) {
216                 if (method.getName().equals("setCompletionStatus")) {
217                     isCompletionStatus = true;
218                 }
219                 String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
220                 Annotation[] annotations = method.getDeclaredAnnotations();
221                 List<Constraint> constraints = new ArrayList<>();
222                 boolean isRequired = false;
223                 for (Annotation annotation : annotations) {
224                     if (annotation instanceof Constraint) {
225                         constraints.add((Constraint) annotation);
226                     }
227                     if (annotation instanceof Required) {
228                         isRequired = true;
229                     }
230                 }
231                 props.add(new Property(name, isRequired, constraints));
232             }
233         }
234         if (!isCompletionStatus) {
235             props.add(new Property("completionStatus", false, new ArrayList<>()));
236         }
237 
238         classMap.putIfAbsent(intrface, props);
239         return classMap.get(intrface);
240     }
241 
242     private static Class<?> getClass(String className) {
243         try {
244             Class<?> intrface = Class.forName(className);
245             if (AuditEvent.class.isAssignableFrom(intrface)) {
246                 return intrface;
247             }
248             logger.error(className + " is not an AuditEvent");
249         } catch (ClassNotFoundException cnfe) {
250             logger.error("Unable to locate class {}", className);
251         }
252         return null;
253     }
254 
255 	private static class AuditProxy implements InvocationHandler {
256 
257 		private final AuditMessage msg;
258 		private final Class<?> intrface;
259         private AuditExceptionHandler auditExceptionHandler;
260 
261 		AuditProxy(AuditMessage msg, Class<?> intrface, AuditExceptionHandler auditExceptionHandler) {
262 			this.msg = msg;
263 			this.intrface = intrface;
264 			this.auditExceptionHandler = auditExceptionHandler;
265 		}
266 
267         public AuditMessage getMessage() {
268             return msg;
269         }
270 
271 		@Override
272 		public Object invoke(Object o, Method method, Object[] objects) {
273 			if (method.getName().equals("toString") && method.getParameterCount() == 0) {
274 				return msg.toString();
275 			}
276 
277 			if (method.getName().equals("logEvent")) {
278 
279 				runMessageAction(() -> validateEvent(intrface, msg), msg, auditExceptionHandler);
280 
281 				logEvent(msg, auditExceptionHandler);
282                 return null;
283 			}
284 
285             if (method.getName().equals("setCompletionStatus")) {
286                 if (objects == null || objects[0] == null) {
287                     throw new IllegalArgumentException("Missing completion status");
288                 }
289                 String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
290                 msg.put(name, objects[0].toString());
291                 return null;
292             }
293 
294             if (method.getName().equals("setAuditExceptionHandler")) {
295 			    if (objects == null || objects[0] == null) {
296                     auditExceptionHandler = NOOP_EXCEPTION_HANDLER;
297                 } else if (objects[0] instanceof AuditExceptionHandler) {
298 			        auditExceptionHandler = (AuditExceptionHandler) objects[0];
299                 } else {
300 			        throw new IllegalArgumentException(objects[0] + " is not an " + AuditExceptionHandler.class.getName());
301                 }
302                 return null;
303             }
304 
305 			if (method.getName().startsWith("set")) {
306 				runMessageAction(() -> setProperty(method, objects), msg, auditExceptionHandler);
307 				return null;
308 			}
309 
310 			return null;
311 		}
312 
313 		@SuppressWarnings("unchecked")
314 		private void setProperty(Method method, Object[] objects) {
315 			String name = NamingUtils.lowerFirst(NamingUtils.getMethodShortName(method.getName()));
316 			if (objects == null || objects[0] == null) {
317 				throw new IllegalArgumentException("No value to be set for " + name);
318 			}
319 
320 			StringBuilder errors = new StringBuilder();
321 			Annotation[] annotations = method.getDeclaredAnnotations();
322 			for (Annotation annotation : annotations) {
323 				if (annotation instanceof Constraints) {
324 					Constraints constraints = (Constraints) annotation;
325 					validateConstraints(false, constraints.value(), name, objects[0].toString(),
326 							errors);
327 				} else if (annotation instanceof Constraint) {
328 					Constraint constraint = (Constraint) annotation;
329 					constraintPlugins.validateConstraint(false, constraint.constraintType(),
330 							name, objects[0].toString(), constraint.constraintValue(), errors);
331 				}
332 			}
333 			if (errors.length() > 0) {
334 				throw new ConstraintValidationException(errors.toString());
335 			}
336 
337 			String result;
338 			if (objects[0] instanceof List) {
339 				result = StringUtils.join(objects, ", ");
340 			} else if (objects[0] instanceof Map) {
341 				StructuredDataMessage extra = new StructuredDataMessage(name, null, null);
342 				extra.putAll((Map) objects[0]);
343 				msg.addContent(name, extra);
344 				return;
345 			} else {
346 				result = objects[0].toString();
347 			}
348 
349 			msg.put(name, result);
350 		}
351 	}
352 
353     private static void validateConstraints(boolean isRequestContext, Constraint[] constraints, String name,
354                                             AuditMessage msg, StringBuilder errors) {
355         String value = isRequestContext ? ThreadContext.get(name) : msg.get(name);
356         validateConstraints(isRequestContext, constraints, name, value, errors);
357     }
358 
359     private static void validateConstraints(boolean isRequestContext, Constraint[] constraints, String name,
360                                             String value, StringBuilder errors) {
361         for (Constraint constraint : constraints) {
362             constraintPlugins.validateConstraint(isRequestContext, constraint.constraintType(), name, value,
363                     constraint.constraintValue(), errors);
364         }
365     }
366 
367     private static void validateContextConstraints(Class<?> intrface, StringBuilder buffer) {
368         RequestContextConstraints reqCtxConstraints = intrface.getAnnotation(RequestContextConstraints.class);
369         if (reqCtxConstraints != null) {
370             for (RequestContext ctx : reqCtxConstraints.value()) {
371                 validateContextConstraint(ctx, buffer);
372             }
373         } else {
374             RequestContext ctx = intrface.getAnnotation(RequestContext.class);
375             validateContextConstraint(ctx, buffer);
376         }
377     }
378 
379     private static void validateContextConstraint(RequestContext constraint, StringBuilder errors) {
380         if (constraint == null) {
381             // the request context is not mandatory
382             return;
383         }
384 
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 }