Remove the legacy audit logger (#116191)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
40674b53be
commit
5a9e170d4b
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(), {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in a new issue