Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add masking layout #249

Merged
merged 2 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}