diff --git a/src/main/java/org/wildfly/security/audit/PeriodicRotatingFileAuditEndpoint.java b/src/main/java/org/wildfly/security/audit/PeriodicRotatingFileAuditEndpoint.java new file mode 100644 index 00000000000..35168e9dba7 --- /dev/null +++ b/src/main/java/org/wildfly/security/audit/PeriodicRotatingFileAuditEndpoint.java @@ -0,0 +1,226 @@ +/* + * 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 java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.time.temporal.WeekFields; +import java.util.Date; +import java.util.Locale; + +import static org.wildfly.common.Assert.checkNotNullParam; +import static org.wildfly.security._private.ElytronMessages.audit; + +/** + * An audit endpoint which rotates the log at a preset time interval. + * + * Based on {@link org.jboss.logmanager.handlers.PeriodicSizeRotatingFileHandler}. + * + * @author Jan Kalina + * @author James R. Perkins + * @author Yeray Borges + */ +public class PeriodicRotatingFileAuditEndpoint extends FileAuditEndpoint { + + private final DateTimeFormatter format; + private final Period period; + private final ZoneId timeZone; + private long nextRollover = Long.MAX_VALUE; + private String nextSuffix; + + PeriodicRotatingFileAuditEndpoint(Builder builder) throws IOException { + super(builder); + this.format = builder.format; + this.period = builder.period; + this.timeZone = builder.timeZone; + + final File file = getFile(); + calcNextRollover(file != null && file.lastModified() > 0 ? file.lastModified() : System.currentTimeMillis()); + } + + @Override + protected void preWrite(Date date) { + final long recordMillis = date.getTime(); + if (recordMillis >= nextRollover) { + try { + final File file = getFile(); + if (file == null) { + // no file is set; a direct output stream or writer was specified + return; + } + closeStreams(); // close the original file (some OSes won't let you move/rename a file that is open) + final Path target = file.getParentFile().toPath().resolve(file.getName() + nextSuffix); + Files.move(file.toPath(), target, StandardCopyOption.REPLACE_EXISTING); + setFile(file); + } catch (IOException e) { + audit.unableToRotateLogFile(e); + } + calcNextRollover(recordMillis); + } + } + + /** + * For given time and period obtains time when should be new log file started + */ + private void calcNextRollover(final long fromTime) { + if (period == Period.NEVER || format == null) { + nextRollover = Long.MAX_VALUE; + return; + } + ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(fromTime), timeZone); + nextSuffix = format.format(zonedDateTime); + switch (period) { + case YEAR: + zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.DAYS) + .withDayOfYear(1) + .plus(1, ChronoUnit.YEARS); + break; + case MONTH: + zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.DAYS) + .withDayOfMonth(1) + .plus(1,ChronoUnit.MONTHS); + break; + case WEEK: + zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.DAYS) + .with(TemporalAdjusters.next(WeekFields.of(Locale.getDefault()).getFirstDayOfWeek())); + break; + case DAY: + zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.DAYS) + .plus(1, ChronoUnit.DAYS); + break; + case HALF_DAY: + ZonedDateTime halfDay = ZonedDateTime.from(zonedDateTime).truncatedTo(ChronoUnit.DAYS) + .plus(1, ChronoUnit.HALF_DAYS); + if ( zonedDateTime.isBefore(halfDay) ) { + zonedDateTime = halfDay; + }else{ + zonedDateTime = halfDay.plus(1, ChronoUnit.HALF_DAYS); + } + break; + case HOUR: + zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.HOURS) + .plus(1, ChronoUnit.HOURS); + break; + case MINUTE: + zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.MINUTES) + .plus(1, ChronoUnit.MINUTES); + } + nextRollover = zonedDateTime.toInstant().toEpochMilli(); + } + + /** + * Possible period values. Keep in strictly ascending order of magnitude. + */ + protected enum Period { + MINUTE, + HOUR, + HALF_DAY, + DAY, + WEEK, + MONTH, + YEAR, + NEVER, + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends FileAuditEndpoint.Builder { + DateTimeFormatter format; + Period period = Period.NEVER; + ZoneId timeZone = ZoneId.systemDefault(); + + Builder() { + super(); + } + + /** + * Set the configured time zone for this handler. + * + * @param timeZone the configured time zone + * @return this builder. + */ + public Builder setTimeZone(ZoneId timeZone) { + this.timeZone = checkNotNullParam("timeZone", timeZone); + + return this; + } + + /** + * Set the suffix string. The string is in a format which can be understood by {@link java.time.format.DateTimeFormatter}. + * The period of the rotation is automatically calculated based on the suffix. + * The following characters \/:*?"<>| are always replaced by '_' + * + * @param suffix the suffix + * @throws IllegalArgumentException if the suffix is not valid + */ + public Builder setSuffix(String suffix) throws IllegalArgumentException { + format = DateTimeFormatter.ofPattern(suffix.replaceAll("[\\/:*?\"<>|]", "_")).withZone(timeZone); + final int len = suffix.length(); + period = Period.NEVER; + for (int i = 0; i < len; i ++) { + switch (suffix.charAt(i)) { + case 'y': period = min(period, Period.YEAR); break; + case 'M': period = min(period, Period.MONTH); break; + case 'w': + case 'W': period = min(period, Period.WEEK); break; + case 'D': + case 'd': + case 'F': + case 'E': period = min(period, Period.DAY); break; + case 'a': period = min(period, Period.HALF_DAY); break; + case 'H': + case 'k': + case 'K': + case 'h': period = min(period, Period.HOUR); break; + case 'm': period = min(period, Period.MINUTE); break; + case '\'': while (suffix.charAt(++i) != '\''){} break; + case 's': + case 'S': throw audit.rotatingBySecondUnsupported(suffix); + } + } + + return this; + } + + /** + * Construct a new instance. + * + * @return the built audit endpoint. + * @throws IOException if an I/O error occurs. + */ + @Override + public AuditEndpoint build() throws IOException { + return new PeriodicRotatingFileAuditEndpoint(this); + } + } + + private static > T min(T a, T b) { + return a.compareTo(b) <= 0 ? a : b; + } +} \ No newline at end of file diff --git a/src/main/java/org/wildfly/security/audit/SizeRotatingFileAuditEndpoint.java b/src/main/java/org/wildfly/security/audit/SizeRotatingFileAuditEndpoint.java new file mode 100644 index 00000000000..0ded051bc0f --- /dev/null +++ b/src/main/java/org/wildfly/security/audit/SizeRotatingFileAuditEndpoint.java @@ -0,0 +1,205 @@ +/* + * 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 java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +import static org.wildfly.common.Assert.checkNotNullParam; +import static org.wildfly.security._private.ElytronMessages.audit; + +/** + * An audit endpoint which rotates the log at the size of the log. + * + * Based on {@link org.jboss.logmanager.handlers.PeriodicSizeRotatingFileHandler}. + * + * @author Jan Kalina + * @author James R. Perkins + * @author Yeray Borges + */ +public class SizeRotatingFileAuditEndpoint extends FileAuditEndpoint { + private final long rotateSize; + private final int maxBackupIndex; + private final boolean rotateOnBoot; + private long currentSize = 0; + private final String suffix; + private final DateTimeFormatter dateTimeFormatter; + + SizeRotatingFileAuditEndpoint(Builder builder) throws IOException { + super(builder); + this.rotateSize = builder.rotateSize; + this.maxBackupIndex = builder.maxBackupIndex; + this.rotateOnBoot = builder.rotateOnBoot; + this.suffix = builder.suffix; + this.dateTimeFormatter = this.suffix != null ? DateTimeFormatter.ofPattern(this.suffix).withZone(builder.timeZone) : null; + + final File file = getFile(); + if (rotateOnBoot && maxBackupIndex > 0 && file != null && file.exists() && file.length() > 0L) { + rotate(file); + } + } + + @Override + protected void write(byte[] bytes) throws IOException { + super.write(bytes); + currentSize += bytes.length; + } + + @Override + protected void preWrite(Date date) { + if (currentSize > rotateSize && maxBackupIndex > 0) { + try { + final File file = getFile(); + if (file == null) { + // no file is set; a direct output stream or writer was specified + return; + } + rotate(file); + currentSize = 0; + } catch (IOException e) { + audit.unableToRotateLogFile(e); + } + } + } + + /** + * Moves file to file.1, file.1 to file.2 etc. Removes file.{maxBackupIndex} + */ + private void rotate(final File file) throws IOException { + closeStreams(); + final String suffix = dateTimeFormatter != null ? dateTimeFormatter.format(ZonedDateTime.now()) : ""; + final Path fileWithSuffix = Paths.get(file.getAbsolutePath() + suffix); + Files.deleteIfExists(Paths.get(fileWithSuffix + "." + maxBackupIndex)); + for (int i = maxBackupIndex - 1; i >= 1; i--) { + final Path src = Paths.get(fileWithSuffix + "." + i); + if (Files.exists(src)) { + final Path target = Paths.get(fileWithSuffix + "." + (i + 1)); + Files.move(src, target, StandardCopyOption.REPLACE_EXISTING); + } + } + Files.move(file.toPath(), Paths.get(fileWithSuffix + ".1"), StandardCopyOption.REPLACE_EXISTING); + setFile(file); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends FileAuditEndpoint.Builder { + + private long rotateSize = 0xa0000L; // 10 MB by default + private int maxBackupIndex = 1; + private boolean rotateOnBoot; + private String suffix; + + ZoneId timeZone = ZoneId.systemDefault(); + + Builder() { + super(); + } + + /** + * Set the log file size the file should rotate at. + * + * @param rotateSize the size the file should rotate at + * @return this builder. + */ + public Builder setRotateSize(long rotateSize) { + this.rotateSize = rotateSize; + + return this; + } + + /** + * Sets the suffix to be appended to the file name during the file rotation. The suffix does not play a role in + * determining when the file should be rotated. + * The following characters \/:*?"<>| are always replaced by '_' + *

+ * The suffix must be a string understood by the {@link java.time.format.DateTimeFormatter}. + *

+ * Note: Files will be rotated for the same suffix until reach the maximum backup index configured {@see #setMaxBackupIndex(int)}. + * If the suffix is resolved to a new value, any files rotated with a different suffix will not be deleted. + * For example if the suffix is .yyyy-DD-mm, the maximum size was reached 20 times on the same day and the maxBackupIndex + * was set to 10, then there will only be 10 files kept. What will not be purged is files from a previous day. + * + * @param suffix the suffix to place after the filename when the file is rotated + */ + public Builder setSuffix(String suffix){ + this.suffix = suffix.replaceAll("[\\/:*?\"<>|]", "_"); + + return this; + } + + /** + * Set the maximum number of files to backup. + * + * @param maxBackupIndex the maximum number of files to backup + * @return this builder. + */ + public Builder setMaxBackupIndex(int maxBackupIndex) { + this.maxBackupIndex = maxBackupIndex; + + return this; + } + + /** + * Set to a value of {@code true} if the file should be rotated before the a new file is set. The rotation only + * happens if the file names are the same and the file has a {@link java.io.File#length() length} greater than 0. + * + * @param rotateOnBoot {@code true} to rotate on boot, otherwise {@code false} + * @return this builder. + */ + public SizeRotatingFileAuditEndpoint.Builder setRotateOnBoot(boolean rotateOnBoot) { + this.rotateOnBoot = rotateOnBoot; + + return this; + } + + /** + * Set the configured time zone for this handler. + * + * @param timeZone the configured time zone + * @return this builder. + */ + public SizeRotatingFileAuditEndpoint.Builder setTimeZone(ZoneId timeZone) { + this.timeZone = checkNotNullParam("timeZone", timeZone); + + return this; + } + + /** + * Construct a new instance. + * + * @return the built audit endpoint. + * @throws IOException if an I/O error occurs. + */ + + @Override + public AuditEndpoint build() throws IOException { + return new SizeRotatingFileAuditEndpoint(this); + } + } +} diff --git a/src/test/java/org/wildfly/security/audit/PeriodicRotatingFileAuditEndpointTest.java b/src/test/java/org/wildfly/security/audit/PeriodicRotatingFileAuditEndpointTest.java new file mode 100644 index 00000000000..49ef74aa605 --- /dev/null +++ b/src/test/java/org/wildfly/security/audit/PeriodicRotatingFileAuditEndpointTest.java @@ -0,0 +1,237 @@ +/* + * 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 mockit.Mock; +import mockit.MockUp; +import mockit.integration.junit4.JMockit; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + + +/** + * Test case to test {@link PeriodicRotatingFileAuditEndpoint} + * + * @author Jan Kalina + * @author Yeray Borges + */ +@RunWith(JMockit.class) +public class PeriodicRotatingFileAuditEndpointTest { + static File logDirFile; + static Path logFile; + static ZoneId UTC = ZoneId.of("UTC"); + Instant currentTime; + + @BeforeClass + public static void init() throws Exception { + Locale.setDefault(Locale.US); + logDirFile = new File(PeriodicRotatingFileAuditEndpointTest.class.getResource(".").getFile(), "audit"); + logFile = logDirFile.toPath().resolve( "audit"); + } + + @Before + public void initDir() { + logDirFile.mkdirs(); + Assert.assertTrue(logDirFile.isDirectory()); + for (File file : logDirFile.listFiles()) { + file.delete(); + } + assertFiles(); + } + + @Test + public void testBase() throws Exception { + AuditEndpoint endpoint = PeriodicRotatingFileAuditEndpoint.builder() + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message"); + endpoint.close(); + assertFiles("audit"); + } + + @Test + public void testTimeBasedRolloverYear() throws Exception { + AuditEndpoint endpoint = PeriodicRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setSuffix(".yyyy") + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message 1"); + currentTime = currentTime.plus(365,ChronoUnit.DAYS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 2"); + currentTime = currentTime.plus(365,ChronoUnit.DAYS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 3"); + endpoint.close(); + assertFiles("audit", "audit.1970","audit.1971"); + } + + @Test + public void testTimeBasedRolloverMonth() throws Exception { + AuditEndpoint endpoint = PeriodicRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setSuffix(".yyyy-MM") + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message 1"); + currentTime = currentTime.plus(32,ChronoUnit.DAYS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 2"); + currentTime = currentTime.plus(32,ChronoUnit.DAYS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 3"); + endpoint.close(); + assertFiles("audit", "audit.1970-01","audit.1970-02"); + } + + @Test + public void testTimeBasedRolloverWeek() throws Exception { + AuditEndpoint endpoint = PeriodicRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setSuffix(".yyyy-MM-ww") + .setLocation(logFile) + .build(); + //1 January 1970 is a Thursday + endpoint.accept(EventPriority.CRITICAL, "testing log message 1"); + currentTime = currentTime.plus(4,ChronoUnit.DAYS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 2"); + currentTime = currentTime.plus(7,ChronoUnit.DAYS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 3"); + endpoint.close(); + assertFiles("audit", "audit.1970-01-01","audit.1970-01-02"); + } + + @Test + public void testTimeBasedRolloverDay() throws Exception { + AuditEndpoint endpoint = PeriodicRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setSuffix(".yyyy-MM-ww-dd") + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message 1"); + currentTime = currentTime.plus(1,ChronoUnit.DAYS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 2"); + currentTime = currentTime.plus(1,ChronoUnit.DAYS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 3"); + endpoint.close(); + assertFiles("audit", "audit.1970-01-01-01","audit.1970-01-01-02"); + } + + @Test + public void testTimeBasedRolloverHalfDay() throws Exception { + AuditEndpoint endpoint = PeriodicRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setSuffix(".yyyy-MM-ww-dd-a") + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message 1"); + currentTime = currentTime.plus(12,ChronoUnit.HOURS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 2"); + currentTime = currentTime.plus(12,ChronoUnit.HOURS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 3"); + endpoint.close(); + assertFiles("audit", "audit.1970-01-01-01-AM","audit.1970-01-01-01-PM"); + } + + @Test + public void testTimeBasedRolloverHour() throws Exception { + AuditEndpoint endpoint = PeriodicRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setSuffix(".yyyy-MM-ww-dd-a-hh") + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message 1"); + currentTime = currentTime.plus(1,ChronoUnit.HOURS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 2"); + currentTime = currentTime.plus(1,ChronoUnit.HOURS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 3"); + endpoint.close(); + assertFiles("audit", "audit.1970-01-01-01-AM-12","audit.1970-01-01-01-AM-01"); + } + + @Test + public void testTimeBasedRolloverHour24() throws Exception { + AuditEndpoint endpoint = PeriodicRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setSuffix(".yyyy-MM-ww-dd-a-HH") + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message 1"); + currentTime = currentTime.plus(1,ChronoUnit.HOURS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 2"); + currentTime = currentTime.plus(1,ChronoUnit.HOURS); + endpoint.accept(EventPriority.CRITICAL, "testing log message 3"); + endpoint.close(); + assertFiles("audit", "audit.1970-01-01-01-AM-00","audit.1970-01-01-01-AM-01"); + } + + @Test + public void testTimeBasedRolloverMinutes() throws Exception { + AuditEndpoint endpoint = PeriodicRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setSuffix(".yyyy-MM-ww-dd-a-HH:mm") + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message 1"); + currentTime = currentTime.plus(1,ChronoUnit.MINUTES); + endpoint.accept(EventPriority.CRITICAL, "testing log message 2"); + currentTime = currentTime.plus(1,ChronoUnit.MINUTES); + endpoint.accept(EventPriority.CRITICAL, "testing log message 3"); + endpoint.close(); + assertFiles("audit", "audit.1970-01-01-01-AM-00_00","audit.1970-01-01-01-AM-00_01"); + } + + @Before + public void mockTime() { + currentTime = Instant.EPOCH.truncatedTo(ChronoUnit.DAYS); + new MockUp() { + @Mock + public long currentTimeMillis() { + return currentTime.toEpochMilli(); + } + }; + new MockUp() { + @Mock + public long lastModified() { + return currentTime.toEpochMilli(); + } + }; + } + + private void assertFiles(String...files) { + Set expected = new HashSet<>(Arrays.asList(files)); + for (File file : logDirFile.listFiles()) { + if (! expected.remove(file.getName())) { + Assert.fail("Unexpected file "+file.getName()); + } + } + for (String missing : expected) { + Assert.fail("Missing file "+missing); + } + } +} diff --git a/src/test/java/org/wildfly/security/audit/SizeRotatingFileAuditEndpointTest.java b/src/test/java/org/wildfly/security/audit/SizeRotatingFileAuditEndpointTest.java new file mode 100644 index 00000000000..a659fd412c4 --- /dev/null +++ b/src/test/java/org/wildfly/security/audit/SizeRotatingFileAuditEndpointTest.java @@ -0,0 +1,158 @@ +/* + * 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 mockit.Mock; +import mockit.MockUp; +import mockit.integration.junit4.JMockit; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Test case to test {@link SizeRotatingFileAuditEndpointTest} + * + * @author Jan Kalina + * @author Yeray Borges + */ +@RunWith(JMockit.class) +public class SizeRotatingFileAuditEndpointTest { + static File logDirFile; + static Path logFile; + static ZoneId UTC = ZoneId.of("UTC"); + Instant currentTime; + + @BeforeClass + public static void init() throws Exception { + logDirFile = new File(SizeRotatingFileAuditEndpointTest.class.getResource(".").getFile(), "audit"); + logFile = Paths.get(logDirFile.getPath(), "audit"); + } + + @Test + public void testBase() throws Exception { + AuditEndpoint endpoint = SizeRotatingFileAuditEndpoint.builder() + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message"); + endpoint.close(); + assertFiles("audit"); + } + + @Test + public void testRotateOnSizeOverflow() throws Exception { + AuditEndpoint endpoint = SizeRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setMaxBackupIndex(4) + .setRotateSize(60) + .setSuffix(".yyyy-MM-dd") + .setLocation(logFile) + .build(); + int i = 0; + for (;i < 15; i++) { + endpoint.accept(EventPriority.CRITICAL, "testing log message "+i); + } + currentTime = currentTime.plus(1, ChronoUnit.DAYS); + for (;i < 30; i++) { + endpoint.accept(EventPriority.CRITICAL, "testing log message "+i); + } + endpoint.close(); + assertFiles("audit", "audit.1970-01-01.1", "audit.1970-01-01.2", "audit.1970-01-01.3", "audit.1970-01-01.4", + "audit.1970-01-02.1", "audit.1970-01-02.2", "audit.1970-01-02.3", "audit.1970-01-02.4"); + } + + @Test + public void testRotateOnBoot() throws Exception { + AuditEndpoint endpoint = SizeRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setRotateOnBoot(true) + .setMaxBackupIndex(2) + .setRotateSize(1) + .setSuffix(".yyyy-MM-dd") + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message 1"); + endpoint.close(); + endpoint = SizeRotatingFileAuditEndpoint.builder() + .setTimeZone(UTC) + .setRotateOnBoot(true) + .setMaxBackupIndex(2) + .setRotateSize(1) + .setSuffix(".yyyy-MM-dd") + .setLocation(logFile) + .build(); + endpoint.accept(EventPriority.CRITICAL, "testing log message 2"); + endpoint.close(); + assertFiles("audit", "audit.1970-01-01.1"); + } + + @Before + public void initDir() { + logDirFile.mkdirs(); + Assert.assertTrue(logDirFile.isDirectory()); + File[] var1 = logDirFile.listFiles(); + int var2 = var1.length; + + for(int var3 = 0; var3 < var2; ++var3) { + File file = var1[var3]; + file.delete(); + } + + this.assertFiles(new String[0]); + } + + @Before + public void mockTime() { + currentTime = Instant.EPOCH.truncatedTo(ChronoUnit.DAYS); + new MockUp() { + @Mock + public long currentTimeMillis() { + return currentTime.toEpochMilli(); + } + }; + new MockUp() { + @Mock + public long lastModified() { + return currentTime.toEpochMilli(); + } + }; + } + + private void assertFiles(String...files) { + Set expected = new HashSet<>(Arrays.asList(files)); + for (File file : logDirFile.listFiles()) { + if (! expected.remove(file.getName())) { + Assert.fail("Unexpected file "+file.getName()); + } + } + for (String missing : expected) { + Assert.fail("Missing file "+missing); + } + } +} \ No newline at end of file