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

Security usage data #110548

Merged
merged 12 commits into from
Sep 1, 2021
88 changes: 67 additions & 21 deletions src/core/server/core_usage_data/core_usage_data_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Side Public License, v 1.
*/

import type { ConfigPath } from '@kbn/config';
import { BehaviorSubject, Observable } from 'rxjs';
import { HotObservable } from 'rxjs/internal/testing/HotObservable';
import { TestScheduler } from 'rxjs/testing';
Expand All @@ -29,12 +30,31 @@ import { CORE_USAGE_STATS_TYPE } from './constants';
import { CoreUsageStatsClient } from './core_usage_stats_client';

describe('CoreUsageDataService', () => {
function getConfigServiceAtPathMockImplementation() {
return (path: ConfigPath) => {
if (path === 'elasticsearch') {
return new BehaviorSubject(RawElasticsearchConfig.schema.validate({}));
} else if (path === 'server') {
return new BehaviorSubject(RawHttpConfig.schema.validate({}));
} else if (path === 'logging') {
return new BehaviorSubject(RawLoggingConfig.schema.validate({}));
} else if (path === 'savedObjects') {
return new BehaviorSubject(RawSavedObjectsConfig.schema.validate({}));
} else if (path === 'kibana') {
return new BehaviorSubject(RawKibanaConfig.schema.validate({}));
}
return new BehaviorSubject({});
};
}

const getTestScheduler = () =>
new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});

let service: CoreUsageDataService;
let configService: ReturnType<typeof configServiceMock.create>;

const mockConfig = {
unused_config: {},
elasticsearch: { username: 'kibana_system', password: 'changeme' },
Expand All @@ -60,27 +80,11 @@ describe('CoreUsageDataService', () => {
},
};

const configService = configServiceMock.create({
getConfig$: mockConfig,
});

configService.atPath.mockImplementation((path) => {
if (path === 'elasticsearch') {
return new BehaviorSubject(RawElasticsearchConfig.schema.validate({}));
} else if (path === 'server') {
return new BehaviorSubject(RawHttpConfig.schema.validate({}));
} else if (path === 'logging') {
return new BehaviorSubject(RawLoggingConfig.schema.validate({}));
} else if (path === 'savedObjects') {
return new BehaviorSubject(RawSavedObjectsConfig.schema.validate({}));
} else if (path === 'kibana') {
return new BehaviorSubject(RawKibanaConfig.schema.validate({}));
}
return new BehaviorSubject({});
});
const coreContext = mockCoreContext.create({ configService });

beforeEach(() => {
configService = configServiceMock.create({ getConfig$: mockConfig });
configService.atPath.mockImplementation(getConfigServiceAtPathMockImplementation());

const coreContext = mockCoreContext.create({ configService });
service = new CoreUsageDataService(coreContext);
});

Expand Down Expand Up @@ -150,7 +154,7 @@ describe('CoreUsageDataService', () => {

describe('start', () => {
describe('getCoreUsageData', () => {
it('returns core metrics for default config', async () => {
function setup() {
const http = httpServiceMock.createInternalSetupContract();
const metrics = metricsServiceMock.createInternalSetupContract();
const savedObjectsStartPromise = Promise.resolve(
Expand Down Expand Up @@ -208,6 +212,11 @@ describe('CoreUsageDataService', () => {
exposedConfigsToUsage: new Map(),
elasticsearch,
});
return { getCoreUsageData };
}

it('returns core metrics for default config', async () => {
const { getCoreUsageData } = setup();
expect(getCoreUsageData()).resolves.toMatchInlineSnapshot(`
Object {
"config": Object {
Expand Down Expand Up @@ -241,6 +250,7 @@ describe('CoreUsageDataService', () => {
"truststoreConfigured": false,
"verificationMode": "full",
},
"username": "none",
},
"http": Object {
"basePathConfigured": false,
Expand Down Expand Up @@ -354,6 +364,42 @@ describe('CoreUsageDataService', () => {
}
`);
});

describe('elasticsearch.username', () => {
async function doTest({ username, expected }: { username: string; expected: string }) {
const defaultMockImplementation = getConfigServiceAtPathMockImplementation();
configService.atPath.mockImplementation((path) => {
if (path === 'elasticsearch') {
return new BehaviorSubject(RawElasticsearchConfig.schema.validate({ username }));
}
return defaultMockImplementation(path);
});
const { getCoreUsageData } = setup();
return expect(getCoreUsageData()).resolves.toEqual(
expect.objectContaining({
config: expect.objectContaining({
elasticsearch: expect.objectContaining({ username: expected }),
}),
})
);
}

it('returns expected usage data for "elastic"', async () => {
return doTest({ username: 'elastic', expected: 'elastic' });
});

it('returns expected usage data for "kibana"', async () => {
return doTest({ username: 'kibana', expected: 'kibana' });
});

it('returns expected usage data for "kibana_system"', async () => {
return doTest({ username: 'kibana_system', expected: 'kibana_system' });
});

it('returns expected usage data for anything else', async () => {
return doTest({ username: 'anything else', expected: 'other' });
});
});
});

describe('getConfigsUsageData', () => {
Expand Down
18 changes: 18 additions & 0 deletions src/core/server/core_usage_data/core_usage_data_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
CoreUsageDataStart,
CoreUsageDataSetup,
ConfigUsageData,
CoreConfigUsageData,
} from './types';
import { isConfigured } from './is_configured';
import { ElasticsearchServiceStart } from '../elasticsearch';
Expand Down Expand Up @@ -253,6 +254,7 @@ export class CoreUsageDataService implements CoreService<CoreUsageDataSetup, Cor
truststoreConfigured: isConfigured.record(es.ssl.truststore),
keystoreConfigured: isConfigured.record(es.ssl.keystore),
},
username: getEsUsernameUsage(es.username),
jportner marked this conversation as resolved.
Show resolved Hide resolved
},
http: {
basePathConfigured: isConfigured.string(http.basePath),
Expand Down Expand Up @@ -512,3 +514,19 @@ export class CoreUsageDataService implements CoreService<CoreUsageDataSetup, Cor
this.stop$.complete();
}
}

function getEsUsernameUsage(username: string | undefined) {
let value: CoreConfigUsageData['elasticsearch']['username'] = 'none';
if (isConfigured.string(username)) {
switch (username) {
case 'elastic': // deprecated
case 'kibana': // deprecated
case 'kibana_system':
value = username;
break;
default:
value = 'other';
}
}
return value;
}
1 change: 1 addition & 0 deletions src/core/server/core_usage_data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export interface CoreConfigUsageData {
};
apiVersion: string;
healthCheckDelayMs: number;
username: 'none' | 'elastic' | 'kibana' | 'kibana_system' | 'other';
jportner marked this conversation as resolved.
Show resolved Hide resolved
};

http: {
Expand Down
1 change: 1 addition & 0 deletions src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ export interface CoreConfigUsageData {
};
apiVersion: string;
healthCheckDelayMs: number;
username: 'none' | 'elastic' | 'kibana' | 'kibana_system' | 'other';
};
// (undocumented)
http: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ export function getCoreUsageCollector(
'The interval in miliseconds between health check requests Kibana sends to the Elasticsearch.',
},
},
username: {
type: 'keyword',
_meta: { description: 'Indicates what elasticsearch username is configured, if any.' },
},
},

http: {
Expand Down
6 changes: 6 additions & 0 deletions src/plugins/telemetry/schema/oss_plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -5957,6 +5957,12 @@
"_meta": {
"description": "The interval in miliseconds between health check requests Kibana sends to the Elasticsearch."
}
},
"username": {
"type": "keyword",
"_meta": {
"description": "Indicates what elasticsearch username is configured, if any."
}
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,16 +357,53 @@ describe('Security UsageCollector', () => {
});

describe('audit logging', () => {
it('reports when audit logging is enabled', async () => {
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, allowAuditLogging: true });
const license = createSecurityLicense({
isLicenseAvailable: true,
allowLegacyAuditLogging: true,
allowAuditLogging: true,
});
registerSecurityUsageCollector({ usageCollection, config, license });

const usage = await usageCollection
.getCollectorByType('security')
?.fetch(collectorFetchContext);

expect(usage).toEqual({
auditLoggingEnabled: true,
auditLoggingType: 'legacy',
accessAgreementEnabled: false,
authProviderCount: 1,
enabledAuthProviders: ['basic'],
loginSelectorEnabled: false,
httpAuthSchemes: ['apikey'],
});
});

it('reports when ECS audit logging is enabled (and legacy audit logging is not enabled)', async () => {
const config = createSecurityConfig(
ConfigSchema.validate({
audit: {
enabled: true,
appender: { type: 'console', layout: { type: 'json' } },
},
})
);
const usageCollection = usageCollectionPluginMock.createSetupContract();
const license = createSecurityLicense({
isLicenseAvailable: true,
allowLegacyAuditLogging: true,
allowAuditLogging: true,
});
registerSecurityUsageCollector({ usageCollection, config, license });

const usage = await usageCollection
Expand All @@ -375,6 +412,7 @@ describe('Security UsageCollector', () => {

expect(usage).toEqual({
auditLoggingEnabled: true,
auditLoggingType: 'ecs',
accessAgreementEnabled: false,
authProviderCount: 1,
enabledAuthProviders: ['basic'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { ConfigType } from '../config';

interface Usage {
auditLoggingEnabled: boolean;
auditLoggingType?: 'ecs' | 'legacy';
Comment on lines 14 to +15
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we already have auditLoggingEnabled and we can't change the existing mapping for that field, I added a second optional field for auditLoggingType. The vast majority of clusters do not have audit logging enabled.

loginSelectorEnabled: boolean;
accessAgreementEnabled: boolean;
authProviderCount: number;
Expand Down Expand Up @@ -58,6 +59,13 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens
'Indicates if audit logging is both enabled and supported by the current license.',
},
},
auditLoggingType: {
type: 'keyword',
_meta: {
description:
'If auditLoggingEnabled is true, indicates what type is enabled (ECS or legacy).',
},
},
loginSelectorEnabled: {
type: 'boolean',
_meta: {
Expand Down Expand Up @@ -118,9 +126,16 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens
}

const legacyAuditLoggingEnabled = allowLegacyAuditLogging && config.audit.enabled;
const auditLoggingEnabled =
const ecsAuditLoggingEnabled =
allowAuditLogging && config.audit.enabled && config.audit.appender != null;

let auditLoggingType: Usage['auditLoggingType'];
if (ecsAuditLoggingEnabled) {
auditLoggingType = 'ecs';
} else if (legacyAuditLoggingEnabled) {
auditLoggingType = 'legacy';
}

const loginSelectorEnabled = config.authc.selector.enabled;
const authProviderCount = config.authc.sortedProviders.length;
const enabledAuthProviders = [
Expand All @@ -140,7 +155,8 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens
);

return {
auditLoggingEnabled: legacyAuditLoggingEnabled || auditLoggingEnabled,
auditLoggingEnabled: legacyAuditLoggingEnabled || ecsAuditLoggingEnabled,
auditLoggingType,
loginSelectorEnabled,
accessAgreementEnabled,
authProviderCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5455,6 +5455,12 @@
"description": "Indicates if audit logging is both enabled and supported by the current license."
}
},
"auditLoggingType": {
"type": "keyword",
"_meta": {
"description": "If auditLoggingEnabled is true, indicates what type is enabled (ECS or legacy)."
}
},
"loginSelectorEnabled": {
"type": "boolean",
"_meta": {
Expand Down