/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Alan Harder
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.plugins.audit_trail;
import hudson.Extension;
import hudson.Plugin;
import hudson.logging.LogRecorder;
import hudson.logging.LogRecorderManager;
import hudson.logging.WeakLogHandler;
import hudson.model.Cause;
import hudson.model.CauseAction;
import hudson.model.Descriptor.FormException;
import hudson.model.Hudson;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.listeners.RunListener;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.PluginServletFilter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import java.util.logging.Formatter;
import java.util.regex.Pattern;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import net.sf.json.JSONObject;
import org.acegisecurity.context.SecurityContextHolder;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
/**
* Keep audit trail of particular Hudson operations, such as configuring jobs.
* @author Alan.Harder@sun.com
*/
public class AuditTrailPlugin extends Plugin {
private String log = "", pattern = ".*/(?:configSubmit|doDelete|postBuildResult|"
+ "cancelQueue|stop|toggleLogKeep|doWipeOutWorkspace|createItem|createView|toggleOffline)";
private int limit = 1, count = 1;
private boolean logBuildCause = true;
private transient ServletContext context;
public String getLog() { return log; }
public int getLimit() { return limit; }
public int getCount() { return count; }
public String getPattern() { return pattern; }
public boolean getLogBuildCause() { return logBuildCause; }
@Override public void setServletContext(ServletContext context) {
this.context = context;
}
@Override public void start() throws Exception {
load();
applySettings();
// Add Filter to watch all requests and log matching ones
PluginServletFilter.addFilter(new AuditTrailFilter());
}
@Override public void postInitialize() {
// Add LogRecorder if not already configured.. but wait for Hudson to initialize:
new Thread() {
@Override public void run() {
try { Thread.sleep(20000); } catch (InterruptedException ex) { }
SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM);
LogRecorderManager lrm = Hudson.getInstance().getLog();
if (!lrm.logRecorders.containsKey("Audit Trail")) {
LogRecorder logRecorder = new LogRecorder("Audit Trail");
logRecorder.targets.add(
new LogRecorder.Target(AuditTrailFilter.class.getPackage().getName(), Level.CONFIG));
try { logRecorder.save(); } catch (Exception ex) { }
lrm.logRecorders.put("Audit Trail", logRecorder);
}
SecurityContextHolder.clearContext();
}
}.start();
}
@Override public void configure(StaplerRequest req, JSONObject formData)
throws IOException, ServletException, FormException {
log = formData.optString("log");
limit = formData.optInt("limit", 1);
count = formData.optInt("count", 1);
pattern = formData.optString("pattern");
logBuildCause = formData.optBoolean("logBuildCause", true);
save();
applySettings();
}
private void applySettings() {
try {
AuditTrailFilter.uriPattern = Pattern.compile(pattern);
}
catch (Exception ex) { ex.printStackTrace(); }
LISTENER.setActive(logBuildCause);
Logger logger = Logger.getLogger(AuditTrailFilter.class.getPackage().getName());
for (Handler handler : logger.getHandlers()) {
logger.removeHandler(handler);
handler.close();
}
if (log != null && log.length() > 0) try {
FileHandler handler = new FileHandler(log, limit * 1024 * 1024, count, true);
handler.setLevel(Level.CONFIG);
handler.setFormatter(new Formatter() {
SimpleDateFormat dateformat = new SimpleDateFormat("MMM d, yyyy h:mm:ss aa ");
public synchronized String format(LogRecord record) {
return dateformat.format(new Date(record.getMillis()))
+ record.getMessage() + '\n';
}
});
logger.setLevel(Level.CONFIG);
logger.addHandler(handler);
// Workaround for SJSWS logging.. no need for audit trail to appear in logs/errors
// since we have our own log file, BUT this handler ignores its level setting and
// logs anything it receives. So don't use parent handlers..
logger.setUseParentHandlers(false);
// ..but Hudson's LogRecorders run via a handler on the root logger so we'll
// route messages directly to that handler..
logger.addHandler(new RouteToHudsonHandler());
}
catch (IOException ex) { ex.printStackTrace(); }
}
private static class RouteToHudsonHandler extends Handler {
public void publish(LogRecord record) {
for (Handler handler : Logger.getLogger("").getHandlers()) {
if (handler instanceof WeakLogHandler) {
handler.publish(record);
}
}
}
public void flush() { }
public void close() { }
}
@Extension public static final AuditTrailRunListener LISTENER = new AuditTrailRunListener();
public static class AuditTrailRunListener extends RunListener<Run> {
private boolean active = false;
private Logger LOG = Logger.getLogger(AuditTrailRunListener.class.getName());
private AuditTrailRunListener() {
super(Run.class);
}
private void setActive(boolean active) {
this.active = active;
}
@Override
public void onStarted(Run run, TaskListener listener) {
if (this.active) {
StringBuilder buf = new StringBuilder(100);
for (CauseAction action : run.getActions(CauseAction.class)) {
for (Cause cause : action.getCauses()) {
if (buf.length() > 0) buf.append(", ");
buf.append(cause.getShortDescription());
}
}
if (buf.length() == 0) buf.append("Started");
LOG.config(run.getParent().getUrl() + " #" + run.getNumber() + ' ' + buf.toString());
}
}
}
/**
* Validate regular expression syntax.
*/
public FormValidation doRegexCheck(@QueryParameter final String value)
throws IOException, ServletException {
// No permission needed for simple syntax check
try {
Pattern.compile(value);
return FormValidation.ok();
}
catch (Exception ex) {
return FormValidation.errorWithMarkup("Invalid <a href=\""
+ "http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/Pattern.html"
+ "\">regular expression</a> (" + ex.getMessage() + ")");
}
}
}