Skip to content

Commit

Permalink
Merge pull request #249 from xm-online/feature/log_masking_master
Browse files Browse the repository at this point in the history
Add masking layout
  • Loading branch information
sergeysenja1992 committed Apr 5, 2024
2 parents bbb001b + 2996839 commit f0a2a82
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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<ILoggingEvent>) it;
Encoder<ILoggingEvent> encoder = appender.getEncoder();
if (encoder instanceof LayoutWrappingEncoder) {
LayoutWrappingEncoder<ILoggingEvent> layoutEncoder = (LayoutWrappingEncoder<ILoggingEvent>) encoder;
Layout<ILoggingEvent> layout = layoutEncoder.getLayout();

LayoutWrappingEncoder<ILoggingEvent> maskingEncoder = new LayoutWrappingEncoder<>();
maskingEncoder.setLayout(new MaskingLayout(layout, loggingConfigService));
maskingEncoder.setContext(context);
maskingEncoder.setCharset(layoutEncoder.getCharset());
maskingEncoder.start();

appender.setEncoder(maskingEncoder);
}
}
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -27,22 +30,32 @@
@Primary
public class LoggingRefreshableConfiguration implements RefreshableConfiguration, LoggingConfigService {

private final MaskingService NULL_MASKING_SERVICE;

private final Map<String, Map<String, LogConfiguration>> serviceLoggingConfig = new ConcurrentHashMap<>();
private final Map<String, Map<String, LogConfiguration>> apiLoggingConfig = new ConcurrentHashMap<>();
private final Map<String, Map<String, LepLogConfiguration>> lepLoggingConfig = new ConcurrentHashMap<>();
private final Map<String, MaskingService> maskingConfig = new ConcurrentHashMap<>();

private final AntPathMatcher matcher = new AntPathMatcher();
private final ObjectMapper ymlMapper = new ObjectMapper(new YAMLFactory());

private final TenantContextHolder tenantContextHolder;
private final String mappingPath;
private final String appName;
private final List<String> 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<String> 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
Expand All @@ -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) {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogConfiguration> serviceLoggingConfigs;

Expand Down Expand Up @@ -59,6 +62,12 @@ private Map<String, LogConfiguration> buildLogConfiguration(List<LogConfiguratio
configuration -> configuration));
}

@Data
public static class MaskingLogConfiguration {
private Boolean enabled;
private List<String> maskPatterns;
}

@Data
public static class LepLogConfiguration {
private String group;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ILoggingEvent> {

private final Layout<ILoggingEvent> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> defaultMaskPatterns) {
List<String> maskPatterns = getMaskPatterns(maskingLogConfiguration);
maskPatterns.addAll(defaultMaskPatterns);
this.enabled = isEnabled(maskingLogConfiguration);
this.multilinePattern = Pattern.compile(String.join("|", maskPatterns), Pattern.MULTILINE);
}

private static List<String> getMaskPatterns(MaskingLogConfiguration maskingLogConfiguration) {
List<String> 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();
}
}

0 comments on commit f0a2a82

Please sign in to comment.