Improve secure logging
[lgpl/argeo-commons.git] / security / runtime / org.argeo.security.core / src / main / java / org / argeo / security / log4j / SecureLogger.java
index 70945f07c31f29181be3413dc820c46f67d81958..a03d2b6d208401857cb4c8258444e9d61bb4f2e0 100644 (file)
 
 package org.argeo.security.log4j;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
 import java.util.Properties;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
 
 import org.apache.log4j.AppenderSkeleton;
 import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
 import org.apache.log4j.PropertyConfigurator;
 import org.apache.log4j.spi.LoggingEvent;
 import org.argeo.ArgeoException;
 import org.argeo.ArgeoLogListener;
-import org.springframework.security.Authentication;
-import org.springframework.security.context.SecurityContext;
-import org.springframework.security.context.SecurityContextHolder;
-import org.springframework.security.providers.anonymous.AnonymousAuthenticationToken;
+import org.argeo.ArgeoLogger;
+import org.argeo.security.SecurityUtils;
 
 /** Not meant to be used directly in standard log4j config */
-public class SecureLogger {
-       private List<ArgeoLogListener> listeners;
+public class SecureLogger implements ArgeoLogger {
 
        private Boolean disabled = false;
 
@@ -47,6 +53,18 @@ public class SecureLogger {
 
        private AppenderImpl appender;
 
+       private final List<ArgeoLogListener> everythingListeners = Collections
+                       .synchronizedList(new ArrayList<ArgeoLogListener>());
+       private final List<ArgeoLogListener> allUsersListeners = Collections
+                       .synchronizedList(new ArrayList<ArgeoLogListener>());
+       private final Map<String, List<ArgeoLogListener>> userListeners = Collections
+                       .synchronizedMap(new HashMap<String, List<ArgeoLogListener>>());
+
+       private BlockingQueue<LogEvent> events;
+       private LogDispatcherThread logDispatcherThread = new LogDispatcherThread();
+
+       private Integer maxLastEventsCount = 10 * 1000;
+
        /** Marker to prevent stack overflow */
        private ThreadLocal<Boolean> dispatching = new ThreadLocal<Boolean>() {
 
@@ -58,16 +76,18 @@ public class SecureLogger {
 
        public void init() {
                try {
+                       events = new LinkedBlockingQueue<LogEvent>();
+
                        // if (layout != null)
                        // setLayout(layout);
                        // else
                        // setLayout(new PatternLayout(pattern));
                        appender = new AppenderImpl();
-
-                       if (configuration != null)
-                               PropertyConfigurator.configure(configuration);
-
+                       reloadConfiguration();
                        Logger.getRootLogger().addAppender(appender);
+
+                       logDispatcherThread = new LogDispatcherThread();
+                       logDispatcherThread.start();
                } catch (Exception e) {
                        throw new ArgeoException("Cannot initialize log4j");
                }
@@ -75,12 +95,71 @@ public class SecureLogger {
 
        public void destroy() throws Exception {
                Logger.getRootLogger().removeAppender(appender);
+               allUsersListeners.clear();
+               for (List<ArgeoLogListener> lst : userListeners.values())
+                       lst.clear();
+               userListeners.clear();
+
+               events.clear();
+               events = null;
+               logDispatcherThread.interrupt();
        }
 
        // public void setLayout(Layout layout) {
        // this.layout = layout;
        // }
 
+       public synchronized void register(ArgeoLogListener listener,
+                       Integer numberOfPreviousEvents) {
+               String username = SecurityUtils.getCurrentThreadUsername();
+               if (username == null)
+                       throw new ArgeoException(
+                                       "Only authenticated users can register a log listener");
+
+               if (!userListeners.containsKey(username)) {
+                       List<ArgeoLogListener> lst = Collections
+                                       .synchronizedList(new ArrayList<ArgeoLogListener>());
+                       userListeners.put(username, lst);
+               }
+               userListeners.get(username).add(listener);
+               List<LogEvent> lastEvents = logDispatcherThread.getLastEvents(username,
+                               numberOfPreviousEvents);
+               for (LogEvent evt : lastEvents)
+                       dispatchEvent(listener, evt);
+       }
+
+       public synchronized void registerForAll(ArgeoLogListener listener,
+                       Integer numberOfPreviousEvents, boolean everything) {
+               if (everything)
+                       everythingListeners.add(listener);
+               else
+                       allUsersListeners.add(listener);
+               List<LogEvent> lastEvents = logDispatcherThread.getLastEvents(null,
+                               numberOfPreviousEvents);
+               for (LogEvent evt : lastEvents)
+                       if (everything || evt.getUsername() != null)
+                               dispatchEvent(listener, evt);
+       }
+
+       public synchronized void unregister(ArgeoLogListener listener) {
+               String username = SecurityUtils.getCurrentThreadUsername();
+               if (!userListeners.containsKey(username))
+                       throw new ArgeoException("No user listeners " + listener
+                                       + " registered for user " + username);
+               if (!userListeners.get(username).contains(listener))
+                       throw new ArgeoException("No user listeners " + listener
+                                       + " registered for user " + username);
+               userListeners.get(username).remove(listener);
+               if (userListeners.get(username).isEmpty())
+                       userListeners.remove(username);
+
+       }
+
+       public synchronized void unregisterForAll(ArgeoLogListener listener) {
+               everythingListeners.remove(listener);
+               allUsersListeners.remove(listener);
+       }
+
        /** For development purpose, since using regular logging is not easy here */
        static void stdOut(Object obj) {
                System.out.println(obj);
@@ -98,14 +177,87 @@ public class SecureLogger {
                this.level = level;
        }
 
-       public void setListeners(List<ArgeoLogListener> listeners) {
-               this.listeners = listeners;
-       }
-
        public void setConfiguration(Properties configuration) {
                this.configuration = configuration;
        }
 
+       public void updateConfiguration(Properties configuration) {
+               setConfiguration(configuration);
+               reloadConfiguration();
+       }
+
+       public Properties getConfiguration() {
+               return configuration;
+       }
+
+       /** Reloads configuration (if the configuration {@link Properties} is set) */
+       protected void reloadConfiguration() {
+               if (configuration != null) {
+                       LogManager.resetConfiguration();
+                       PropertyConfigurator.configure(configuration);
+               }
+       }
+
+       protected synchronized void processLoggingEvent(LogEvent event) {
+               if (disabled)
+                       return;
+
+               if (dispatching.get())
+                       return;
+
+               if (level != null && !level.trim().equals("")) {
+                       if (log4jLevel == null || !log4jLevel.toString().equals(level))
+                               try {
+                                       log4jLevel = Level.toLevel(level);
+                               } catch (Exception e) {
+                                       System.err
+                                                       .println("Log4j level could not be set for level '"
+                                                                       + level + "', resetting it to null.");
+                                       e.printStackTrace();
+                                       level = null;
+                               }
+
+                       if (log4jLevel != null
+                                       && !event.getLoggingEvent().getLevel()
+                                                       .isGreaterOrEqual(log4jLevel)) {
+                               return;
+                       }
+               }
+
+               try {
+                       // admin listeners
+                       Iterator<ArgeoLogListener> everythingIt = everythingListeners
+                                       .iterator();
+                       while (everythingIt.hasNext())
+                               dispatchEvent(everythingIt.next(), event);
+
+                       if (event.getUsername() != null) {
+                               Iterator<ArgeoLogListener> allUsersIt = allUsersListeners
+                                               .iterator();
+                               while (allUsersIt.hasNext())
+                                       dispatchEvent(allUsersIt.next(), event);
+
+                               if (userListeners.containsKey(event.getUsername())) {
+                                       Iterator<ArgeoLogListener> userIt = userListeners.get(
+                                                       event.getUsername()).iterator();
+                                       while (userIt.hasNext())
+                                               dispatchEvent(userIt.next(), event);
+                               }
+                       }
+               } catch (Exception e) {
+                       stdOut("Cannot process logging event");
+                       e.printStackTrace();
+               }
+       }
+
+       protected void dispatchEvent(ArgeoLogListener logListener, LogEvent evt) {
+               LoggingEvent event = evt.getLoggingEvent();
+               logListener.appendLog(evt.getUsername(), event.getTimeStamp(), event
+                               .getLevel().toString(), event.getLoggerName(), event
+                               .getThreadName(), event.getMessage(), event
+                               .getThrowableStrRep());
+       }
+
        private class AppenderImpl extends AppenderSkeleton {
                public boolean requiresLayout() {
                        return false;
@@ -116,61 +268,93 @@ public class SecureLogger {
 
                @Override
                protected void append(LoggingEvent event) {
-                       if (disabled)
-                               return;
+                       if (events != null) {
+                               try {
+                                       String username = SecurityUtils.getCurrentThreadUsername();
+                                       events.put(new LogEvent(username, event));
+                               } catch (InterruptedException e) {
+                                       // silent
+                               }
+                       }
+               }
 
-                       if (dispatching.get())
-                               return;
+       }
 
-                       if (level != null && !level.trim().equals("")) {
-                               if (log4jLevel == null || !log4jLevel.toString().equals(level))
-                                       try {
-                                               log4jLevel = Level.toLevel(level);
-                                       } catch (Exception e) {
-                                               System.err
-                                                               .println("Log4j level could not be set for level '"
-                                                                               + level + "', resetting it to null.");
-                                               e.printStackTrace();
-                                               level = null;
-                                       }
-
-                               if (log4jLevel != null
-                                               && !event.getLevel().isGreaterOrEqual(log4jLevel)) {
-                                       return;
+       private class LogDispatcherThread extends Thread {
+               /** encapsulated in order to simplify concurrency management */
+               private LinkedList<LogEvent> lastEvents = new LinkedList<LogEvent>();
+
+               public LogDispatcherThread() {
+                       super("Argeo Logging Dispatcher Thread");
+               }
+
+               public void run() {
+                       while (events != null) {
+                               try {
+                                       LogEvent loggingEvent = events.take();
+                                       processLoggingEvent(loggingEvent);
+                                       addLastEvent(loggingEvent);
+                               } catch (InterruptedException e) {
+                                       if (events == null)
+                                               return;
                                }
                        }
+               }
 
-                       try {
-                               Thread currentThread = Thread.currentThread();
-                               String username = null;
-
-                               // find username
-                               SecurityContext securityContext = SecurityContextHolder
-                                               .getContext();
-                               if (securityContext != null) {
-                                       Authentication authentication = securityContext
-                                                       .getAuthentication();
-                                       if (authentication != null) {
-                                               if (authentication instanceof AnonymousAuthenticationToken) {
-                                                       username = null;
-                                               } else {
-                                                       username = authentication.getName();
-                                               }
-                                       }
-                               }
+               protected synchronized void addLastEvent(LogEvent loggingEvent) {
+                       if (lastEvents.size() >= maxLastEventsCount)
+                               lastEvents.poll();
+                       lastEvents.add(loggingEvent);
+               }
 
-                               // Spring OSGi safe
-                               Iterator<ArgeoLogListener> it = listeners.iterator();
-                               while (it.hasNext()) {
-                                       ArgeoLogListener logListener = it.next();
-                                       logListener.appendLog(username, event.getTimeStamp(), event
-                                                       .getLevel().toString(), event.getLoggerName(),
-                                                       currentThread.getName(), event.getMessage(), event.getThrowableStrRep());
+               public synchronized List<LogEvent> getLastEvents(String username,
+                               Integer maxCount) {
+                       LinkedList<LogEvent> evts = new LinkedList<LogEvent>();
+                       ListIterator<LogEvent> it = lastEvents.listIterator(lastEvents
+                                       .size());
+                       int count = 0;
+                       while (it.hasPrevious() && (count < maxCount)) {
+                               LogEvent evt = it.previous();
+                               if (username == null || username.equals(evt.getUsername())) {
+                                       evts.push(evt);
+                                       count++;
                                }
-                       } catch (Exception e) {
-                               stdOut("Cannot process logging event");
-                               e.printStackTrace();
                        }
+                       return evts;
+               }
+       }
+
+       private class LogEvent {
+               private final String username;
+               private final LoggingEvent loggingEvent;
+
+               public LogEvent(String username, LoggingEvent loggingEvent) {
+                       super();
+                       this.username = username;
+                       this.loggingEvent = loggingEvent;
+               }
+
+               @Override
+               public int hashCode() {
+                       return loggingEvent.hashCode();
+               }
+
+               @Override
+               public boolean equals(Object obj) {
+                       return loggingEvent.equals(obj);
+               }
+
+               @Override
+               public String toString() {
+                       return username + "@ " + loggingEvent.toString();
+               }
+
+               public String getUsername() {
+                       return username;
+               }
+
+               public LoggingEvent getLoggingEvent() {
+                       return loggingEvent;
                }
 
        }