org.wildfly.security.audit.FileAuditEndpoint Maven / Gradle / Ivy
The newest version!
/*
* JBoss, Home of Professional Open Source.
* Copyright 2017 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.wildfly.security.audit;
import static org.wildfly.common.Assert.checkNotNullParam;
import static org.wildfly.security.audit.ElytronMessages.audit;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.function.Supplier;
/**
* An audit endpoint to record all audit events to a local file.
*
* @author Darran Lofthouse
*/
public class FileAuditEndpoint implements AuditEndpoint {
private static final String LINE_TERMINATOR = System.lineSeparator();
private volatile boolean accepting = true;
private final Supplier dateTimeFormatterSupplier;
private final boolean syncOnAccept;
private final boolean flushOnAccept;
private File file;
private FileDescriptor fileDescriptor;
private Writer writer;
private Charset charset;
/** Clock providing access to current time. */
protected final Clock clock;
FileAuditEndpoint(Builder builder) throws IOException {
this.dateTimeFormatterSupplier = builder.dateTimeFormatterSupplier;
this.syncOnAccept = builder.syncOnAccept;
this.flushOnAccept = builder.flushOnAccept;
this.clock = builder.clock;
this.charset = builder.charset != null ? builder.charset : StandardCharsets.UTF_8;
setFile(builder.location.toFile());
}
void setFile(final File file) throws IOException {
boolean isFileSet = false;
final FileOutputStream fos = new FileOutputStream(file, true);
try {
final Writer writer = new OutputStreamWriter(new BufferedOutputStream(fos), this.charset);
try {
this.fileDescriptor = fos.getFD();
this.writer = writer;
this.file = file;
isFileSet = true;
} finally {
if (! isFileSet) {
safeClose(writer);
}
}
} finally {
if (! isFileSet) {
safeClose(fos);
}
}
}
File getFile() {
return file;
}
private void safeClose(Closeable c) {
try {
if (c != null) c.close();
} catch (Exception e) {
audit.trace("Unable to close", e);
}
}
/**
* Method called to write given String to the target local file.
* This method can be overridden by subclasses to modify data written into file (to encrypt them for example),
* or just for counting amount of written bytes for needs of log rotation and similar.
*
* This method can be invoked only in synchronization block surrounding one log message processing.
*
* @param toWrite the String to be written into the target local file
*/
void write(String toWrite) throws IOException {
writer.write(toWrite);
}
/**
* Method called before writing into local file.
* This method is NO-OP by default. It is intended to be overridden by subclasses
* which need to perform some operation before every writing into the target local file.
*
* This method can be invoked only in synchronization block surrounding one log message processing.
*
* @param instant time of the message acceptance
*/
void preWrite(Instant instant) {
// NO-OP by default
}
/**
* Accept formatted security event message to be processed written into target local file.
*
* @param priority priority of the logged message
* @param message the logged message
* @throws IOException when writing into the target local file fails
*/
@Override
public void accept(EventPriority priority, String message) throws IOException {
if (!accepting) return;
Instant instant = clock.instant();
StringBuffer buffer = new StringBuffer();
buffer.append(dateTimeFormatterSupplier.get().format(instant));
buffer.append(',');
buffer.append(priority.toString());
buffer.append(',');
buffer.append(message);
buffer.append(LINE_TERMINATOR);
String toWrite = buffer.toString();
synchronized(this) {
if (!accepting) return; // We may have been waiting to get in here.
preWrite(instant);
write(toWrite);
if (flushOnAccept) writer.flush();
if (syncOnAccept) fileDescriptor.sync();
}
}
@Override
public void close() throws IOException {
accepting = false;
synchronized (this) {
closeStreams();
}
}
/**
* Close opened file streams. Can be called by subclasses for needs of target file changing.
* Must be called in synchronized block together with reopening using {@code setFile()}.
*/
void closeStreams() throws IOException {
writer.flush();
fileDescriptor.sync();
writer.close();
}
/**
* Obtain a new {@link Builder} capable of building a {@link FileAuditEndpoint}.
*
* @return a new {@link Builder} capable of building a {@link FileAuditEndpoint}.
*/
public static Builder builder() {
return new Builder();
}
/**
* A builder for file audit endpoints.
*/
public static class Builder {
private Clock clock = Clock.systemUTC();
private Supplier dateTimeFormatterSupplier = () -> DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault());
private Path location = new File("audit.log").toPath();
private boolean syncOnAccept = true;
private boolean flushOnAccept = true;
private boolean flushSet = false;
private Charset charset;
Builder() {
}
/**
* Set the supplier to obtain the {@link DateTimeFormatter} for dates.
* The supplied formatter has to have a time zone configured.
*
* @param dateTimeFormatterSupplier the supplier to obtain the {@link DateTimeFormatter}
* @return this builder.
*/
public Builder setDateTimeFormatterSupplier(Supplier dateTimeFormatterSupplier) {
this.dateTimeFormatterSupplier = checkNotNullParam("dateTimeFormatterSupplier", dateTimeFormatterSupplier);
return this;
}
/**
* Set the location to write the audit events to.
*
* @param location the location to write the audit events to.
* @return this builder.
*/
public Builder setLocation(Path location) {
this.location = checkNotNullParam("location", location);
return this;
}
/**
* Sets if the output should be flushed on each event accepted.
* If not set, flushing is done when output buffers synchronization is set.
*
* @param flushOnAccept should the output be flushed on each event accepted.
* @return this builder.
* @since 1.3.0
*/
public Builder setFlushOnAccept(boolean flushOnAccept) {
this.flushOnAccept = flushOnAccept;
this.flushSet = true;
return this;
}
/**
* Sets if the system output buffers should be forced to be synchronized on each event accepted. Enabled by default.
* Output flushing can be set independently using {@link #setFlushOnAccept(boolean)} but defaults to this value.
*
* @param syncOnAccept should the system output buffers be forced to be synchronized on each event accepted.
* @return this builder.
*/
public Builder setSyncOnAccept(boolean syncOnAccept) {
this.syncOnAccept = syncOnAccept;
if (! flushSet) this.flushOnAccept = syncOnAccept;
return this;
}
/**
* Sets the {@link Clock} instance the resulting {@link FileAuditEndpoint} should use to query the current time.
* For testing purposes only, therefore package visible.
*
* @param clock the clock to query the current time
* @return this builder
*/
Builder setClock(Clock clock) {
this.clock = clock;
return this;
}
/**
* Set the file's character set.
*
* @param charset the character set
* @return this builder.
*/
public Builder setCharset(Charset charset) {
this.charset = charset;
return this;
}
/**
* Construct a new file audit endpoint.
*
* @return the built file audit endpoint.
*/
public AuditEndpoint build() throws IOException {
return new FileAuditEndpoint(this);
}
}
}