Add an option to disable APM user redaction (#176566)

## Summary

Fix https://github.com/elastic/kibana/issues/174743

Add an `elastic.apm.redactUsers` configuration option that defaults to
`true` (preserving current behavior)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pierre Gayvallet 2024-02-18 16:30:01 +01:00 committed by GitHub
parent aae8639263
commit d65460cb39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 68 additions and 13 deletions

View file

@ -24,4 +24,5 @@ const apmReusableConfigSchema = schema.object(
export const apmConfigSchema = apmReusableConfigSchema.extends({
servicesOverrides: schema.maybe(schema.recordOf(schema.string(), apmReusableConfigSchema)),
redactUsers: schema.maybe(schema.boolean({ defaultValue: true })),
});

View file

@ -405,4 +405,30 @@ describe('ApmConfiguration', () => {
);
});
});
describe('isUsersRedactionEnabled', () => {
it('defaults to true', () => {
const kibanaConfig = {
elastic: {
apm: {},
},
};
const config = new ApmConfiguration(mockedRootDir, kibanaConfig, false);
expect(config.isUsersRedactionEnabled()).toEqual(true);
});
it('uses the value defined in the config if specified', () => {
const kibanaConfig = {
elastic: {
apm: {
redactUsers: false,
},
},
};
const config = new ApmConfiguration(mockedRootDir, kibanaConfig, false);
expect(config.isUsersRedactionEnabled()).toEqual(false);
});
});
});

View file

@ -93,6 +93,11 @@ export class ApmConfiguration {
return baseConfig;
}
public isUsersRedactionEnabled(): boolean {
const { redactUsers = true } = this.getConfigFromKibanaConfig();
return redactUsers;
}
private getBaseConfig() {
if (!this.baseConfig) {
const configFromSources = this.getConfigFromAllSources();
@ -287,7 +292,8 @@ export class ApmConfiguration {
* Reads APM configuration from different sources and merges them together.
*/
private getConfigFromAllSources(): AgentConfigOptions {
const { servicesOverrides, ...configFromKibanaConfig } = this.getConfigFromKibanaConfig();
const { servicesOverrides, redactUsers, ...configFromKibanaConfig } =
this.getConfigFromKibanaConfig();
const configFromEnv = this.getConfigFromEnv(configFromKibanaConfig);
const config = merge({}, configFromKibanaConfig, configFromEnv);

View file

@ -15,14 +15,17 @@ describe('initApm', () => {
let apmAddFilterMock: jest.Mock;
let apmStartMock: jest.Mock;
let getConfig: jest.Mock;
let isUsersRedactionEnabled: jest.Mock;
beforeEach(() => {
apmAddFilterMock = apm.addFilter as jest.Mock;
apmStartMock = apm.start as jest.Mock;
getConfig = jest.fn();
isUsersRedactionEnabled = jest.fn();
mockLoadConfiguration.mockImplementation(() => ({
getConfig,
isUsersRedactionEnabled,
}));
});
@ -46,13 +49,29 @@ describe('initApm', () => {
expect(getConfig).toHaveBeenCalledWith('service-name');
});
it('registers a filter using `addFilter`', () => {
it('calls `apmConfigLoader.isUsersRedactionEnabled`', () => {
initApm(['foo', 'bar'], 'rootDir', true, 'service-name');
expect(isUsersRedactionEnabled).toHaveBeenCalledTimes(1);
});
it('registers a filter using `addFilter` when user redaction is enabled', () => {
isUsersRedactionEnabled.mockReturnValue(true);
initApm(['foo', 'bar'], 'rootDir', true, 'service-name');
expect(apmAddFilterMock).toHaveBeenCalledTimes(1);
expect(apmAddFilterMock).toHaveBeenCalledWith(expect.any(Function));
});
it('does not register a filter using `addFilter` when user redaction is disabled', () => {
isUsersRedactionEnabled.mockReturnValue(false);
initApm(['foo', 'bar'], 'rootDir', true, 'service-name');
expect(apmAddFilterMock).not.toHaveBeenCalled();
});
it('starts apm with the config returned from `getConfig`', () => {
const config = {
foo: 'bar',

View file

@ -16,24 +16,27 @@ export const initApm = (
) => {
const apmConfigLoader = loadConfiguration(argv, rootDir, isDistributable);
const apmConfig = apmConfigLoader.getConfig(serviceName);
const shouldRedactUsers = apmConfigLoader.isUsersRedactionEnabled();
// we want to only load the module when effectively used
// eslint-disable-next-line @typescript-eslint/no-var-requires
const apm = require('elastic-apm-node');
// Filter out all user PII
apm.addFilter((payload: Record<string, any>) => {
try {
if (payload.context?.user && typeof payload.context.user === 'object') {
Object.keys(payload.context.user).forEach((key) => {
payload.context.user[key] = '[REDACTED]';
});
if (shouldRedactUsers) {
apm.addFilter((payload: Record<string, any>) => {
try {
if (payload.context?.user && typeof payload.context.user === 'object') {
Object.keys(payload.context.user).forEach((key) => {
payload.context.user[key] = '[REDACTED]';
});
}
} catch (e) {
// just silently ignore the error
}
} catch (e) {
// just silently ignore the error
}
return payload;
});
return payload;
});
}
apm.start(apmConfig);
};