Remove the legacy audit logger (#116191)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Larry Gregory 2021-10-26 13:32:17 -04:00 committed by GitHub
parent 40674b53be
commit 5a9e170d4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 112 additions and 296 deletions

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { BehaviorSubject, Observable, of } from 'rxjs';
import { Observable, of } from 'rxjs';
import {
coreMock,
@ -14,10 +14,9 @@ import {
loggingSystemMock,
} from 'src/core/server/mocks';
import type { SecurityLicenseFeatures } from '../../common/licensing';
import { licenseMock } from '../../common/licensing/index.mock';
import type { ConfigType } from '../config';
import { ConfigSchema } from '../config';
import { ConfigSchema, createConfig } from '../config';
import type { AuditEvent } from './audit_events';
import {
AuditService,
@ -28,13 +27,15 @@ import {
jest.useFakeTimers();
const createConfig = (settings: Partial<ConfigType['audit']>) => {
return ConfigSchema.validate({ audit: settings }).audit;
};
const logger = loggingSystemMock.createLogger();
const license = licenseMock.create();
const config = createConfig({ enabled: true });
const createAuditConfig = (settings: Partial<ConfigType['audit']>) => {
return createConfig(ConfigSchema.validate({ audit: settings }), logger, { isTLSEnabled: false })
.audit;
};
const config = createAuditConfig({ enabled: true });
const { logging } = coreMock.createSetup();
const http = httpServiceMock.createSetupContract();
const getCurrentUser = jest.fn().mockReturnValue({ username: 'jdoe', roles: ['admin'] });
@ -132,6 +133,7 @@ describe('#setup', () => {
license,
config: {
enabled: false,
appender: undefined,
},
logging,
http,
@ -198,6 +200,12 @@ describe('#asScoped', () => {
license,
config: {
enabled: true,
appender: {
type: 'console',
layout: {
type: 'json',
},
},
ignore_filters: [{ actions: ['ACTION'] }],
},
logging,
@ -222,6 +230,12 @@ describe('#asScoped', () => {
license,
config: {
enabled: true,
appender: {
type: 'console',
layout: {
type: 'json',
},
},
ignore_filters: [{ actions: ['ACTION'] }],
},
logging,
@ -306,22 +320,6 @@ describe('#createLoggingConfig', () => {
expect(loggingConfig.loggers![0].level).toEqual('off');
});
test('sets log level to `off` when appender is not defined', async () => {
const features$ = of({
allowAuditLogging: true,
});
const loggingConfig = await features$
.pipe(
createLoggingConfig({
enabled: true,
})
)
.toPromise();
expect(loggingConfig.loggers![0].level).toEqual('off');
});
test('sets log level to `off` when license does not allow audit logging', async () => {
const features$ = of({
allowAuditLogging: false,
@ -563,175 +561,3 @@ describe('#filterEvent', () => {
).toBeFalsy();
});
});
describe('#getLogger', () => {
test('calls the underlying logger with the provided message and requisite tags', () => {
const pluginId = 'foo';
const licenseWithFeatures = licenseMock.create();
licenseWithFeatures.features$ = new BehaviorSubject({
allowLegacyAuditLogging: true,
} as SecurityLicenseFeatures).asObservable();
const auditService = new AuditService(logger).setup({
license: licenseWithFeatures,
config,
logging,
http,
getCurrentUser,
getSpaceId,
getSID,
recordAuditLoggingUsage,
});
const auditLogger = auditService.getLogger(pluginId);
const eventType = 'bar';
const message = 'this is my audit message';
auditLogger.log(eventType, message);
expect(logger.info).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith(message, {
eventType,
tags: [pluginId, eventType],
});
});
test('calls the underlying logger with the provided metadata', () => {
const pluginId = 'foo';
const licenseWithFeatures = licenseMock.create();
licenseWithFeatures.features$ = new BehaviorSubject({
allowLegacyAuditLogging: true,
} as SecurityLicenseFeatures).asObservable();
const auditService = new AuditService(logger).setup({
license: licenseWithFeatures,
config,
logging,
http,
getCurrentUser,
getSpaceId,
getSID,
recordAuditLoggingUsage,
});
const auditLogger = auditService.getLogger(pluginId);
const eventType = 'bar';
const message = 'this is my audit message';
const metadata = Object.freeze({
property1: 'value1',
property2: false,
property3: 123,
});
auditLogger.log(eventType, message, metadata);
expect(logger.info).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith(message, {
eventType,
tags: [pluginId, eventType],
property1: 'value1',
property2: false,
property3: 123,
});
});
test('does not call the underlying logger if license does not support audit logging', () => {
const pluginId = 'foo';
const licenseWithFeatures = licenseMock.create();
licenseWithFeatures.features$ = new BehaviorSubject({
allowLegacyAuditLogging: false,
} as SecurityLicenseFeatures).asObservable();
const auditService = new AuditService(logger).setup({
license: licenseWithFeatures,
config,
logging,
http,
getCurrentUser,
getSpaceId,
getSID,
recordAuditLoggingUsage,
});
const auditLogger = auditService.getLogger(pluginId);
const eventType = 'bar';
const message = 'this is my audit message';
auditLogger.log(eventType, message);
expect(logger.info).not.toHaveBeenCalled();
});
test('does not call the underlying logger if security audit logging is not enabled', () => {
const pluginId = 'foo';
const licenseWithFeatures = licenseMock.create();
licenseWithFeatures.features$ = new BehaviorSubject({
allowLegacyAuditLogging: true,
} as SecurityLicenseFeatures).asObservable();
const auditService = new AuditService(logger).setup({
license: licenseWithFeatures,
config: createConfig({
enabled: false,
}),
logging,
http,
getCurrentUser,
getSpaceId,
getSID,
recordAuditLoggingUsage,
});
const auditLogger = auditService.getLogger(pluginId);
const eventType = 'bar';
const message = 'this is my audit message';
auditLogger.log(eventType, message);
expect(logger.info).not.toHaveBeenCalled();
});
test('calls the underlying logger after license upgrade', () => {
const pluginId = 'foo';
const licenseWithFeatures = licenseMock.create();
const features$ = new BehaviorSubject({
allowLegacyAuditLogging: false,
} as SecurityLicenseFeatures);
licenseWithFeatures.features$ = features$.asObservable();
const auditService = new AuditService(logger).setup({
license: licenseWithFeatures,
config,
logging,
http,
getCurrentUser,
getSpaceId,
getSID,
recordAuditLoggingUsage,
});
const auditLogger = auditService.getLogger(pluginId);
const eventType = 'bar';
const message = 'this is my audit message';
auditLogger.log(eventType, message);
expect(logger.info).not.toHaveBeenCalled();
// perform license upgrade
features$.next({
allowLegacyAuditLogging: true,
} as SecurityLicenseFeatures);
auditLogger.log(eventType, message);
expect(logger.info).toHaveBeenCalledTimes(1);
});
});

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import type { Subscription } from 'rxjs';
import { distinctUntilKeyChanged, map } from 'rxjs/operators';
import type {
@ -58,18 +57,10 @@ interface AuditServiceSetupParams {
}
export class AuditService {
/**
* @deprecated
*/
private licenseFeaturesSubscription?: Subscription;
/**
* @deprecated
*/
private allowLegacyAuditLogging = false;
private ecsLogger: Logger;
private usageIntervalId?: NodeJS.Timeout;
constructor(private readonly logger: Logger) {
constructor(logger: Logger) {
this.ecsLogger = logger.get('ecs');
}
@ -83,14 +74,6 @@ export class AuditService {
getSpaceId,
recordAuditLoggingUsage,
}: AuditServiceSetupParams): AuditServiceSetup {
if (config.enabled && !config.appender) {
this.licenseFeaturesSubscription = license.features$.subscribe(
({ allowLegacyAuditLogging }) => {
this.allowLegacyAuditLogging = allowLegacyAuditLogging;
}
);
}
// Configure logging during setup and when license changes
logging.configure(
license.features$.pipe(
@ -181,17 +164,7 @@ export class AuditService {
*/
const getLogger = (id?: string): LegacyAuditLogger => {
return {
log: (eventType: string, message: string, data?: Record<string, any>) => {
if (!this.allowLegacyAuditLogging) {
return;
}
this.logger.info(message, {
tags: id ? [id, eventType] : [eventType],
eventType,
...data,
});
},
log: (eventType: string, message: string, data?: Record<string, any>) => {},
};
};
@ -206,10 +179,6 @@ export class AuditService {
}
stop() {
if (this.licenseFeaturesSubscription) {
this.licenseFeaturesSubscription.unsubscribe();
this.licenseFeaturesSubscription = undefined;
}
clearInterval(this.usageIntervalId!);
}
}

View file

@ -10,6 +10,10 @@ jest.mock('crypto', () => ({
constants: jest.requireActual('crypto').constants,
}));
jest.mock('@kbn/utils', () => ({
getDataPath: () => '/mock/kibana/data/path',
}));
import { loggingSystemMock } from 'src/core/server/mocks';
import { ConfigSchema, createConfig } from './config';
@ -1703,6 +1707,50 @@ describe('createConfig()', () => {
`);
});
it('creates a default audit appender when audit logging is enabled', () => {
expect(
createConfig(
ConfigSchema.validate({
audit: {
enabled: true,
},
}),
loggingSystemMock.create().get(),
{ isTLSEnabled: true }
).audit.appender
).toMatchInlineSnapshot(`
Object {
"fileName": "/mock/kibana/data/path/audit.log",
"layout": Object {
"type": "json",
},
"policy": Object {
"interval": "PT24H",
"type": "time-interval",
},
"strategy": Object {
"max": 10,
"type": "numeric",
},
"type": "rolling-file",
}
`);
});
it('does not create a default audit appender when audit logging is disabled', () => {
expect(
createConfig(
ConfigSchema.validate({
audit: {
enabled: false,
},
}),
loggingSystemMock.create().get(),
{ isTLSEnabled: true }
).audit.appender
).toBeUndefined();
});
it('accepts an audit appender', () => {
expect(
ConfigSchema.validate({
@ -1741,19 +1789,6 @@ describe('createConfig()', () => {
).toThrow('[audit.appender.1.layout]: expected at least one defined value but got [undefined]');
});
it('rejects an ignore_filter when no appender is configured', () => {
expect(() =>
ConfigSchema.validate({
audit: {
enabled: true,
ignore_filters: [{ actions: ['some_action'] }],
},
})
).toThrow(
'[audit]: xpack.security.audit.ignore_filters can only be used with the ECS audit logger. To enable the ECS audit logger, specify where you want to write the audit events using xpack.security.audit.appender.'
);
});
describe('#getExpirationTimeouts', () => {
function createMockConfig(config: Record<string, any> = {}) {
return createConfig(ConfigSchema.validate(config), loggingSystemMock.createLogger(), {

View file

@ -7,11 +7,13 @@
import crypto from 'crypto';
import type { Duration } from 'moment';
import path from 'path';
import type { Type, TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import type { Logger } from 'src/core/server';
import { getDataPath } from '@kbn/utils';
import type { AppenderConfigType, Logger } from 'src/core/server';
import { config as coreConfig } from '../../../../src/core/server';
import type { AuthenticationProvider } from '../common/model';
@ -271,30 +273,21 @@ export const ConfigSchema = schema.object({
schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey', 'bearer'] }),
}),
}),
audit: schema.object(
{
enabled: schema.boolean({ defaultValue: false }),
appender: schema.maybe(coreConfig.logging.appenders),
ignore_filters: schema.maybe(
schema.arrayOf(
schema.object({
actions: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
categories: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
types: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
outcomes: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
spaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
})
)
),
},
{
validate: (auditConfig) => {
if (auditConfig.ignore_filters && !auditConfig.appender) {
return 'xpack.security.audit.ignore_filters can only be used with the ECS audit logger. To enable the ECS audit logger, specify where you want to write the audit events using xpack.security.audit.appender.';
}
},
}
),
audit: schema.object({
enabled: schema.boolean({ defaultValue: false }),
appender: schema.maybe(coreConfig.logging.appenders),
ignore_filters: schema.maybe(
schema.arrayOf(
schema.object({
actions: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
categories: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
types: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
outcomes: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
spaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
})
)
),
}),
});
export function createConfig(
@ -381,8 +374,29 @@ export function createConfig(
sortedProviders.filter(({ type, name }) => providers[type]?.[name].showInSelector).length >
1;
const appender: AppenderConfigType | undefined =
config.audit.appender ??
({
type: 'rolling-file',
fileName: path.join(getDataPath(), 'audit.log'),
layout: {
type: 'json',
},
policy: {
type: 'time-interval',
interval: schema.duration().validate('24h'),
},
strategy: {
type: 'numeric',
max: 10,
},
} as AppenderConfigType);
return {
...config,
audit: {
...config.audit,
...(config.audit.enabled && { appender }),
},
authc: {
selector: { ...config.authc.selector, enabled: isLoginSelectorEnabled },
providers,

View file

@ -343,35 +343,7 @@ describe('Security UsageCollector', () => {
});
describe('audit logging', () => {
it('reports when legacy audit logging is enabled (and ECS audit logging is not enabled)', async () => {
const config = createSecurityConfig(
ConfigSchema.validate({
audit: {
enabled: true,
appender: undefined,
},
})
);
const usageCollection = usageCollectionPluginMock.createSetupContract();
const license = createSecurityLicense({
isLicenseAvailable: true,
allowLegacyAuditLogging: true,
allowAuditLogging: true,
});
registerSecurityUsageCollector({ usageCollection, config, license });
const usage = await usageCollection
.getCollectorByType('security')
?.fetch(collectorFetchContext);
expect(usage).toEqual({
...DEFAULT_USAGE,
auditLoggingEnabled: true,
auditLoggingType: 'legacy',
});
});
it('reports when ECS audit logging is enabled (and legacy audit logging is not enabled)', async () => {
it('reports when ECS audit logging is enabled', async () => {
const config = createSecurityConfig(
ConfigSchema.validate({
audit: {