diff --git a/xm-commons-logging-configurable/src/main/java/com/icthh/xm/commons/logging/configurable/LogMaskingConfiguration.java b/xm-commons-logging-configurable/src/main/java/com/icthh/xm/commons/logging/configurable/LogMaskingConfiguration.java new file mode 100644 index 00000000..2353ee64 --- /dev/null +++ b/xm-commons-logging-configurable/src/main/java/com/icthh/xm/commons/logging/configurable/LogMaskingConfiguration.java @@ -0,0 +1,46 @@ +package com.icthh.xm.commons.logging.configurable; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Layout; +import ch.qos.logback.core.OutputStreamAppender; +import ch.qos.logback.core.encoder.Encoder; +import ch.qos.logback.core.encoder.LayoutWrappingEncoder; +import com.icthh.xm.commons.logging.config.LoggingConfigService; +import com.icthh.xm.commons.logging.util.MaskingLayout; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE; + +@Slf4j +@Configuration +@Order(HIGHEST_PRECEDENCE) +public class LogMaskingConfiguration { + + public LogMaskingConfiguration(LoggingConfigService loggingConfigService) { + log.info("Init MaskingPatternLayout"); + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + context.getLogger("ROOT").iteratorForAppenders().forEachRemaining(it -> { + if (it instanceof OutputStreamAppender) { + var appender = (OutputStreamAppender) it; + Encoder encoder = appender.getEncoder(); + if (encoder instanceof LayoutWrappingEncoder) { + LayoutWrappingEncoder layoutEncoder = (LayoutWrappingEncoder) encoder; + Layout layout = layoutEncoder.getLayout(); + + LayoutWrappingEncoder maskingEncoder = new LayoutWrappingEncoder<>(); + maskingEncoder.setLayout(new MaskingLayout(layout, loggingConfigService)); + maskingEncoder.setContext(context); + maskingEncoder.setCharset(layoutEncoder.getCharset()); + maskingEncoder.start(); + + appender.setEncoder(maskingEncoder); + } + } + }); + } + +} diff --git a/xm-commons-logging-configurable/src/main/java/com/icthh/xm/commons/logging/configurable/LoggingRefreshableConfiguration.java b/xm-commons-logging-configurable/src/main/java/com/icthh/xm/commons/logging/configurable/LoggingRefreshableConfiguration.java index 4705e5fe..3d83be01 100644 --- a/xm-commons-logging-configurable/src/main/java/com/icthh/xm/commons/logging/configurable/LoggingRefreshableConfiguration.java +++ b/xm-commons-logging-configurable/src/main/java/com/icthh/xm/commons/logging/configurable/LoggingRefreshableConfiguration.java @@ -6,7 +6,9 @@ import com.icthh.xm.commons.logging.config.LoggingConfig; import com.icthh.xm.commons.logging.config.LoggingConfig.LepLogConfiguration; import com.icthh.xm.commons.logging.config.LoggingConfig.LogConfiguration; +import com.icthh.xm.commons.logging.config.LoggingConfig.MaskingLogConfiguration; import com.icthh.xm.commons.logging.config.LoggingConfigService; +import com.icthh.xm.commons.logging.util.MaskingService; import com.icthh.xm.commons.tenant.TenantContextHolder; import com.icthh.xm.commons.tenant.TenantKey; import lombok.extern.slf4j.Slf4j; @@ -17,6 +19,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -27,9 +30,12 @@ @Primary public class LoggingRefreshableConfiguration implements RefreshableConfiguration, LoggingConfigService { + private final MaskingService NULL_MASKING_SERVICE; + private final Map> serviceLoggingConfig = new ConcurrentHashMap<>(); private final Map> apiLoggingConfig = new ConcurrentHashMap<>(); private final Map> lepLoggingConfig = new ConcurrentHashMap<>(); + private final Map maskingConfig = new ConcurrentHashMap<>(); private final AntPathMatcher matcher = new AntPathMatcher(); private final ObjectMapper ymlMapper = new ObjectMapper(new YAMLFactory()); @@ -37,12 +43,19 @@ public class LoggingRefreshableConfiguration implements RefreshableConfiguration private final TenantContextHolder tenantContextHolder; private final String mappingPath; private final String appName; + private final List maskPatterns; public LoggingRefreshableConfiguration(TenantContextHolder tenantContextHolder, - @Value("${spring.application.name}") String appName) { + @Value("${spring.application.name}") String appName, + @Value("${application.maskPatterns:#{T(java.util.Collections).emptyList()}}") + List maskPatterns) { this.tenantContextHolder = tenantContextHolder; this.mappingPath = "/config/tenants/{tenantName}/" + appName + "/logging.yml"; this.appName = appName; + this.maskPatterns = maskPatterns; + MaskingLogConfiguration maskingLogConfiguration = new MaskingLogConfiguration(); + maskingLogConfiguration.setEnabled(!maskPatterns.isEmpty()); + this.NULL_MASKING_SERVICE = new MaskingService(maskingLogConfiguration, maskPatterns); } @Override @@ -60,6 +73,7 @@ public void onRefresh(final String updatedKey, final String config) { this.serviceLoggingConfig.put(tenant, spec.buildServiceLoggingConfigs()); this.apiLoggingConfig.put(tenant, spec.buildApiLoggingConfigs()); this.lepLoggingConfig.put(tenant, spec.buildLepLoggingConfigs(tenant, appName)); + this.maskingConfig.put(tenant, new MaskingService(spec.getMasking(), maskPatterns)); log.info("Tenant configuration was updated for tenant [{}] by key [{}]", tenant, updatedKey); } catch (Exception e) { @@ -79,6 +93,14 @@ public void onInit(final String configKey, final String configValue) { } } + @Override + public MaskingService getMaskingService() { + return getTenantKey(this.tenantContextHolder) + .map(TenantKey::getValue) + .map(maskingConfig::get) + .orElse(NULL_MASKING_SERVICE); + } + @Override public LogConfiguration getServiceLoggingConfig(String packageName, String className, diff --git a/xm-commons-logging-configurable/src/test/java/com/icthh/xm/commons/logging/configurable/LogMaskingIntTest.java b/xm-commons-logging-configurable/src/test/java/com/icthh/xm/commons/logging/configurable/LogMaskingIntTest.java new file mode 100644 index 00000000..0276cf1a --- /dev/null +++ b/xm-commons-logging-configurable/src/test/java/com/icthh/xm/commons/logging/configurable/LogMaskingIntTest.java @@ -0,0 +1,68 @@ +package com.icthh.xm.commons.logging.configurable; + +import com.icthh.xm.commons.security.spring.config.XmAuthenticationContextConfiguration; +import com.icthh.xm.commons.tenant.TenantContextHolder; +import com.icthh.xm.commons.tenant.TenantContextUtils; +import com.icthh.xm.commons.tenant.spring.config.TenantContextConfiguration; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.model.Statement; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.OutputCaptureRule; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; + +@Slf4j +@RunWith(SpringRunner.class) +@SpringBootTest(classes = { + TenantContextConfiguration.class, + LoggingRefreshableConfiguration.class, + XmAuthenticationContextConfiguration.class, + LogMaskingConfiguration.class +}, properties = {"spring.application.name=testApp"}) +public class LogMaskingIntTest { + + private static final String UPDATE_KEY = "/config/tenants/TESTWITHCONFIG/testApp/logging.yml"; + + @Autowired + private TenantContextHolder tenantContextHolder; + @Autowired + private LoggingRefreshableConfiguration loggingRefreshableConfiguration; + + @Test + public void testLogMaskingConfiguration() { + String config = "masking:\n enabled: true\n maskPatterns:\n - \"token=([^,]+)\"\n - \"password=([^,]+)\"\n"; + loggingRefreshableConfiguration.onRefresh(UPDATE_KEY, config); + String originalLog = "Log with password=passwordvalue, token=tokenvalue, attribute=blabla"; + String maskedLog = "Log with password=*************, token=**********, attribute=blabla"; + + writeLogExpectLog(originalLog, originalLog); + + TenantContextUtils.setTenant(tenantContextHolder, "TEST"); + writeLogExpectLog(originalLog, originalLog); + tenantContextHolder.getPrivilegedContext().destroyCurrentContext(); + + TenantContextUtils.setTenant(tenantContextHolder, "TESTWITHCONFIG"); + writeLogExpectLog(originalLog, maskedLog); + } + + @SneakyThrows + private void writeLogExpectLog(String logToWrite, String expectLog) { + String loggerName = LogMaskingIntTest.class.getSimpleName() + ": "; + var outputCapture = new OutputCaptureRule(); + outputCapture.apply(new Statement() { + @Override + public void evaluate() throws Throwable { + log.info(logToWrite); + String actual = outputCapture.toString(); + actual = actual.substring(actual.indexOf(loggerName) + loggerName.length()).trim(); + assertEquals(expectLog, actual); + } + }, Description.EMPTY).evaluate(); + } +} diff --git a/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfig.java b/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfig.java index a10a8fe9..304b9f09 100644 --- a/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfig.java +++ b/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfig.java @@ -22,6 +22,9 @@ public class LoggingConfig { public static final boolean DEFAULT_LOG_RESULT_DETAILS = true; public static final boolean DEFAULT_LOG_RESULT_COLLECTION_AWARE = false; + @JsonProperty("masking") + private MaskingLogConfiguration masking; + @JsonProperty("service") private List serviceLoggingConfigs; @@ -59,6 +62,12 @@ private Map buildLogConfiguration(List configuration)); } + @Data + public static class MaskingLogConfiguration { + private Boolean enabled; + private List maskPatterns; + } + @Data public static class LepLogConfiguration { private String group; diff --git a/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfigService.java b/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfigService.java index 9419a7ca..331a8ae2 100644 --- a/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfigService.java +++ b/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfigService.java @@ -1,11 +1,14 @@ package com.icthh.xm.commons.logging.config; import com.icthh.xm.commons.logging.config.LoggingConfig.LepLogConfiguration; +import com.icthh.xm.commons.logging.util.MaskingService; import static com.icthh.xm.commons.logging.config.LoggingConfig.LogConfiguration; public interface LoggingConfigService { + MaskingService getMaskingService(); + LogConfiguration getServiceLoggingConfig(String packageName, String className, String methodName); LogConfiguration getApiLoggingConfig(String packageName, String className, String methodName); diff --git a/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfigServiceStub.java b/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfigServiceStub.java index d5a7b5a8..3c44d210 100644 --- a/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfigServiceStub.java +++ b/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/config/LoggingConfigServiceStub.java @@ -1,11 +1,18 @@ package com.icthh.xm.commons.logging.config; +import com.icthh.xm.commons.logging.util.MaskingService; + import static com.icthh.xm.commons.logging.config.LoggingConfig.LepLogConfiguration; import static com.icthh.xm.commons.logging.config.LoggingConfig.LogConfiguration; public class LoggingConfigServiceStub implements LoggingConfigService { + @Override + public MaskingService getMaskingService() { + return null; + } + @Override public LogConfiguration getServiceLoggingConfig(String packageName, String className, String methodName) { return null; diff --git a/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/util/MaskingLayout.java b/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/util/MaskingLayout.java new file mode 100644 index 00000000..0fd15b9e --- /dev/null +++ b/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/util/MaskingLayout.java @@ -0,0 +1,23 @@ +package com.icthh.xm.commons.logging.util; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Layout; +import ch.qos.logback.core.LayoutBase; +import com.icthh.xm.commons.logging.config.LoggingConfigService; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MaskingLayout extends LayoutBase { + + private final Layout layout; + private final LoggingConfigService loggingConfigService; + + @Override + public String doLayout(ILoggingEvent event) { + return maskMessage(layout.doLayout(event)); + } + + private String maskMessage(String message) { + return loggingConfigService.getMaskingService().maskMessage(message); + } +} diff --git a/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/util/MaskingService.java b/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/util/MaskingService.java new file mode 100644 index 00000000..bfaf2dda --- /dev/null +++ b/xm-commons-logging/src/main/java/com/icthh/xm/commons/logging/util/MaskingService.java @@ -0,0 +1,54 @@ +package com.icthh.xm.commons.logging.util; + +import com.icthh.xm.commons.logging.config.LoggingConfig.MaskingLogConfiguration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +import static java.util.Collections.emptyList; + +public class MaskingService { + private final Pattern multilinePattern; + private final boolean enabled; + + public MaskingService(MaskingLogConfiguration maskingLogConfiguration, List defaultMaskPatterns) { + List maskPatterns = getMaskPatterns(maskingLogConfiguration); + maskPatterns.addAll(defaultMaskPatterns); + this.enabled = isEnabled(maskingLogConfiguration); + this.multilinePattern = Pattern.compile(String.join("|", maskPatterns), Pattern.MULTILINE); + } + + private static List getMaskPatterns(MaskingLogConfiguration maskingLogConfiguration) { + List patterns = Optional.ofNullable(maskingLogConfiguration) + .map(MaskingLogConfiguration::getMaskPatterns) + .orElse(emptyList()); + return new ArrayList<>(patterns); + } + + private static Boolean isEnabled(MaskingLogConfiguration maskingLogConfiguration) { + return Optional.ofNullable(maskingLogConfiguration) + .map(MaskingLogConfiguration::getEnabled) + .orElse(false); + } + + public String maskMessage(String message) { + if (!enabled) { + return message; + } + + StringBuilder sb = new StringBuilder(message); + Matcher matcher = multilinePattern.matcher(sb); + while (matcher.find()) { + IntStream.rangeClosed(1, matcher.groupCount()).forEach(group -> { + if (matcher.group(group) != null) { + IntStream.range(matcher.start(group), matcher.end(group)).forEach(i -> sb.setCharAt(i, '*')); + } + }); + } + return sb.toString(); + } +}