diff --git a/tc-server/src/main/java/com/tc/l2/logging/SecurityLogger.java b/tc-server/src/main/java/com/tc/l2/logging/SecurityLogger.java new file mode 100644 index 0000000000..6dfe90df5e --- /dev/null +++ b/tc-server/src/main/java/com/tc/l2/logging/SecurityLogger.java @@ -0,0 +1,349 @@ +/* + * Copyright IBM Corp. 2025 + */ +package com.tc.l2.logging; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; + +public class SecurityLogger implements Logger { + + private final String name; + static final Logger securityLogger = LoggerFactory.getLogger("SECURITY_LOGGER"); + + public SecurityLogger(String name) { + if (name == null) { + throw new IllegalArgumentException("Logger name cannot be null"); + } + this.name = name; + } + + private void logWithName(String message, Consumer consumer) { + consumer.accept( name + " - " + message); + } + + private void logWithName(String message, T arg, BiConsumer biConsumer) { + biConsumer.accept(name + " - " + message, arg); + } + + private void logWithName(BiConsumer biConsumer, String format, Object... arguments) { + biConsumer.accept(name + " - " + format, arguments); + } + + private void logWithName(String format, Object arg1, Object arg2, TriConsumer triConsumer) { + triConsumer.accept(name + " - " + format, arg1, arg2); + } + + @FunctionalInterface + public interface TriConsumer { + void accept(T t, U u, V v); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isTraceEnabled() { + return securityLogger.isTraceEnabled(); + } + + @Override + public void trace(String msg) { + logWithName(msg, securityLogger::trace); + } + + @Override + public void trace(String format, Object arg) { + logWithName(format, arg, securityLogger::trace); + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + logWithName(format, arg1, arg1, securityLogger::trace); + } + + @Override + public void trace(String format, Object... arguments) { + logWithName(securityLogger::trace, format, arguments); + } + + @Override + public void trace(String msg, Throwable t) { + logWithName(msg, t, securityLogger::trace); + } + + @Override + public boolean isTraceEnabled(Marker marker) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void trace(Marker marker, String msg) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void trace(Marker marker, String format, Object arg) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void trace(Marker marker, String format, Object... argArray) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void trace(Marker marker, String msg, Throwable t) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isDebugEnabled() { + return securityLogger.isDebugEnabled(); + } + + @Override + public void debug(String msg) { + logWithName(msg, securityLogger::debug); + } + + @Override + public void debug(String format, Object arg) { + logWithName(format, arg, securityLogger::debug); + } + + @Override + public void debug(String format, Object arg1, Object arg2) { + logWithName(format, arg1, arg2, securityLogger::debug); + } + + @Override + public void debug(String format, Object... arguments) { + logWithName(securityLogger::debug, format, arguments); + } + + @Override + public void debug(String msg, Throwable t) { + logWithName(msg, t, securityLogger::debug); + } + + @Override + public boolean isDebugEnabled(Marker marker) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void debug(Marker marker, String msg) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void debug(Marker marker, String format, Object arg) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void debug(Marker marker, String format, Object arg1, Object arg2) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void debug(Marker marker, String format, Object... arguments) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void debug(Marker marker, String msg, Throwable t) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isInfoEnabled() { + return securityLogger.isInfoEnabled(); + } + + @Override + public void info(String msg) { + logWithName(msg, securityLogger::info); + } + + @Override + public void info(String format, Object arg) { + logWithName(format, arg, securityLogger::info); + } + + @Override + public void info(String format, Object arg1, Object arg2) { + logWithName(format, arg1, arg2, securityLogger::info); + } + + @Override + public void info(String format, Object... arguments) { + logWithName(securityLogger::info, format, arguments); + } + + @Override + public void info(String msg, Throwable t) { + logWithName(msg, t, securityLogger::info); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void info(Marker marker, String msg) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void info(Marker marker, String format, Object arg) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void info(Marker marker, String format, Object arg1, Object arg2) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void info(Marker marker, String format, Object... arguments) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void info(Marker marker, String msg, Throwable t) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isWarnEnabled() { + return securityLogger.isWarnEnabled(); + } + + @Override + public void warn(String msg) { + logWithName(msg, securityLogger::warn); + } + + @Override + public void warn(String format, Object arg) { + logWithName(format, arg, securityLogger::warn); + } + + @Override + public void warn(String format, Object... arguments) { + logWithName(securityLogger::warn, format, arguments); + } + + @Override + public void warn(String format, Object arg1, Object arg2) { + logWithName(format, arg1, arg2, securityLogger::warn); + } + + @Override + public void warn(String msg, Throwable t) { + logWithName(msg, t, securityLogger::warn); + } + + @Override + public boolean isWarnEnabled(Marker marker) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void warn(Marker marker, String msg) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void warn(Marker marker, String format, Object arg) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void warn(Marker marker, String format, Object arg1, Object arg2) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void warn(Marker marker, String format, Object... arguments) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void warn(Marker marker, String msg, Throwable t) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isErrorEnabled() { + return securityLogger.isErrorEnabled(); + } + + @Override + public void error(String msg) { + logWithName(msg, securityLogger::error); + } + + @Override + public void error(String format, Object arg) { + logWithName(format, arg, securityLogger::error); + } + + @Override + public void error(String format, Object arg1, Object arg2) { + logWithName(format, arg1, arg2, securityLogger::error); + } + + @Override + public void error(String format, Object... arguments) { + logWithName(securityLogger::error, format, arguments); + } + + @Override + public void error(String msg, Throwable t) { + logWithName(msg, t, securityLogger::error); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void error(Marker marker, String msg) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void error(Marker marker, String format, Object arg) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void error(Marker marker, String format, Object arg1, Object arg2) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void error(Marker marker, String format, Object... arguments) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void error(Marker marker, String msg, Throwable t) { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/tc-server/src/main/java/com/tc/l2/logging/SecurityLoggerFactory.java b/tc-server/src/main/java/com/tc/l2/logging/SecurityLoggerFactory.java new file mode 100644 index 0000000000..5fe7ba84ee --- /dev/null +++ b/tc-server/src/main/java/com/tc/l2/logging/SecurityLoggerFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright IBM Corp. 2025 + */ +package com.tc.l2.logging; + +import org.slf4j.Logger; +import com.tc.logging.TCLogging; + +public class SecurityLoggerFactory { + + private static boolean isSecurityLogEnabled; + + public static Logger getLogger(Class clazz) { + if (clazz == null) { + throw new NullPointerException("Class cannot be null"); + } + return getLogger(clazz.getName()); + } + + public static Logger getLogger(String name) { + if (name == null) { + throw new NullPointerException("Logger name cannot be null"); + } + return isSecurityLogEnabled ? new SecurityLogger(name) : TCLogging.getSilentLogger(); + } + + // exposing this method for testing purpose only + static void setIsSecurityLogEnabled(boolean value) { + isSecurityLogEnabled = value; + } + +} diff --git a/tc-server/src/main/java/com/tc/l2/logging/TCLogbackLogging.java b/tc-server/src/main/java/com/tc/l2/logging/TCLogbackLogging.java index a477386bbc..a47065924a 100644 --- a/tc-server/src/main/java/com/tc/l2/logging/TCLogbackLogging.java +++ b/tc-server/src/main/java/com/tc/l2/logging/TCLogbackLogging.java @@ -22,9 +22,14 @@ import ch.qos.logback.classic.encoder.PatternLayoutEncoder; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; +import ch.qos.logback.core.ConsoleAppender; import ch.qos.logback.core.CoreConstants; +import ch.qos.logback.core.FileAppender; +import ch.qos.logback.core.OutputStreamAppender; +import ch.qos.logback.core.UnsynchronizedAppenderBase; import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; import ch.qos.logback.core.rolling.RollingFileAppender; +import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy; import ch.qos.logback.core.util.FileSize; import com.tc.logging.TCLogging; import org.slf4j.Logger; @@ -42,6 +47,9 @@ public class TCLogbackLogging { public static final String CONSOLE = TCLogging.CONSOLE_LOGGER_NAME; public static final String STDOUT_APPENDER = "STDOUT"; private static final String TC_PATTERN = "%d [%t] %p %c - %m%n"; + private static final String SECURITY_LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n"; + private static final String FILE_EXTENSION = ".log"; + private static final String FILE_ROLLING_INDEX = ".%i"; private static final Logger LOGGER = LoggerFactory.getLogger(CONSOLE); public static void resetLogging() { @@ -114,11 +122,11 @@ public static void redirectLogging(File logDirFile) { String logDir = getPathString(logDirFile); LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); ch.qos.logback.classic.Logger root = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME); - if (logDirFile != null) { - Appender continuingAppender = installFileAppender(logDir, loggerContext); + Appender continuingAppender = installFileAppender(logDir, "terracotta.server", TC_PATTERN, loggerContext); root.addAppender(continuingAppender); disableBufferingAppender(continuingAppender); + installSecurityLogger(logDirFile); } else { disableBufferingAppender(null); } @@ -149,8 +157,8 @@ private static void disableBufferingAppender(Appender continuingA } } - private static Appender installFileAppender(String logDir, LoggerContext loggerContext) { - String logLocation = logDir + File.separator + "terracotta.server.log"; + private static Appender installFileAppender(String logDir, String logFileName, String logPattern, LoggerContext loggerContext) { + String logLocation = logDir + File.separator + logFileName + FILE_EXTENSION; LOGGER.info("Log file: {}", logLocation); RollingFileAppender fileAppender = new RollingFileAppender<>(); @@ -160,7 +168,7 @@ private static Appender installFileAppender(String logDir, Logger PatternLayoutEncoder logEncoder = new PatternLayoutEncoder(); logEncoder.setContext(loggerContext); - logEncoder.setPattern(TC_PATTERN); + logEncoder.setPattern(logPattern); logEncoder.start(); fileAppender.setEncoder(logEncoder); @@ -168,7 +176,7 @@ private static Appender installFileAppender(String logDir, Logger FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy(); rollingPolicy.setMinIndex(1); rollingPolicy.setMaxIndex(20); - rollingPolicy.setFileNamePattern(logDir + File.separator + "terracotta.server.%i.log"); + rollingPolicy.setFileNamePattern(logDir + File.separator + logFileName + FILE_ROLLING_INDEX + FILE_EXTENSION); rollingPolicy.setContext(loggerContext); rollingPolicy.setParent(fileAppender); rollingPolicy.start(); @@ -205,4 +213,26 @@ private static String getPathString(File logPath) { throw new UncheckedIOException("Error getting canonical path for the logging directory", ioe); } } + + private static void installSecurityLogger(File logDirFile) { + String logDir = getPathString(logDirFile); + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + Appender fileAppender = installFileAppender(logDir, "terracotta.server.security", SECURITY_LOG_PATTERN, loggerContext); + + PatternLayoutEncoder logEncoder = new PatternLayoutEncoder(); + logEncoder.setContext(loggerContext); + logEncoder.setPattern(SECURITY_LOG_PATTERN); + logEncoder.start(); + + ConsoleAppender consoleAppender = new ConsoleAppender<>(); + consoleAppender.setContext(loggerContext); + consoleAppender.setEncoder(logEncoder); + consoleAppender.start(); + + ch.qos.logback.classic.Logger securityLogger = loggerContext.getLogger("SECURITY_LOGGER"); + securityLogger.setLevel(Level.DEBUG); + securityLogger.setAdditive(false); + securityLogger.addAppender(fileAppender); + securityLogger.addAppender(consoleAppender); + } } diff --git a/tc-server/src/test/java/com/tc/l2/logging/SecurityLoggerFactoryTest.java b/tc-server/src/test/java/com/tc/l2/logging/SecurityLoggerFactoryTest.java new file mode 100644 index 0000000000..9e3531751b --- /dev/null +++ b/tc-server/src/test/java/com/tc/l2/logging/SecurityLoggerFactoryTest.java @@ -0,0 +1,128 @@ +/* + * Copyright IBM Corp. 2025 + */ +package com.tc.l2.logging; + +import static com.tc.l2.logging.TCLogbackLogging.CONSOLE; +import java.io.File; +import java.io.FileReader; +import java.io.LineNumberReader; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.contrib.java.lang.system.SystemOutRule; +import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SecurityLoggerFactoryTest { + + public @Rule + SystemOutRule sysout = new SystemOutRule().enableLog(); + public @Rule + TemporaryFolder temp = new TemporaryFolder(); + + public SecurityLoggerFactoryTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + TCLogbackLogging.redirectLogging(new File(System.getProperty("user.home") + "/terracotta")); + } + + @After + public void tearDown() { + } + + @Test + public void testSecurityLogAndRootLogSeparation_whenLoggingEnabled() { + SecurityLoggerFactory.setIsSecurityLogEnabled(true); + + Logger securityLogger = SecurityLoggerFactory.getLogger(SecurityLoggerFactoryTest.class); + assertNotNull(securityLogger); + assertTrue(securityLogger instanceof SecurityLogger); + + Logger rootLogger = LoggerFactory.getLogger(SecurityLoggerFactoryTest.class); + assertNotNull(rootLogger); + assertFalse(rootLogger instanceof SecurityLogger); + assertTrue(rootLogger instanceof Logger); + + assertNotEquals(securityLogger, rootLogger); + } + + @Test + public void testGetLoggerWithClass_whenLoggingEnabled() { + SecurityLoggerFactory.setIsSecurityLogEnabled(true); + Logger logger = SecurityLoggerFactory.getLogger(SecurityLoggerFactoryTest.class); + assertNotNull(logger); + assertTrue(logger instanceof SecurityLogger); + } + + @Test + public void testLoggingFunctionality_whenLoggingEnabled() { + + SecurityLoggerFactory.setIsSecurityLogEnabled(true); + Logger LOGGER = LoggerFactory.getLogger(SecurityLoggerFactoryTest.class); + Logger securityLogger = SecurityLoggerFactory.getLogger(SecurityLoggerFactoryTest.class); + + LOGGER.info("info - logging using default logger"); + LOGGER.debug("debug - logging using default logger"); + LOGGER.warn("warn - logging using default logger"); + LOGGER.error("error - logging using default logger"); + + securityLogger.info("info - logging using security logger"); + securityLogger.debug("debug - logging using security logger"); + securityLogger.warn("warn - logging using security logger"); + securityLogger.error("error - logging using security logger"); + } + + @Test + @Ignore("skipping bcus of test failure") + public void testRedirectLogging() throws Exception { + System.out.println("bootstrapLogging"); + TCLogbackLogging.resetLogging(); + TCLogbackLogging.bootstrapLogging(null); + + // test that console logger is properly installed + Logger test = LoggerFactory.getLogger(CONSOLE); + test.info("this is a test"); + assertThat(sysout.getLog(), not(containsString("this is a test"))); + + File folder = temp.newFolder(); + TCLogbackLogging.redirectLogging(folder); + assertThat(sysout.getLog(), containsString("this is a test")); + LoggerFactory.getLogger(CONSOLE).info("flush1"); + LoggerFactory.getLogger(CONSOLE).info("flush2"); + LoggerFactory.getLogger(CONSOLE).info("flush3"); + LoggerFactory.getLogger(CONSOLE).info("flush4"); + + FileReader read = new FileReader(new File(folder, "terracotta.server.security.log")); + LineNumberReader lines = new LineNumberReader(read); + boolean contains = false; + String line = lines.readLine(); + while (line != null) { + System.out.println("TESTING " + line); + if (line.contains("this is a test")) { + contains = true; + break; + } + line = lines.readLine(); + } + assertTrue(contains); + } +}