From c8f7870fcdc20176226ad44b05148cefd43452a1 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Aug 2021 17:01:12 -0400 Subject: [PATCH 01/10] Audit logging usage data --- .../security_usage_collector.test.ts | 42 ++++++++++++++++++- .../security_usage_collector.ts | 20 ++++++++- .../schema/xpack_plugins.json | 6 +++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts index 6a1f6662e796e..c02e644329805 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts @@ -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 @@ -375,6 +412,7 @@ describe('Security UsageCollector', () => { expect(usage).toEqual({ auditLoggingEnabled: true, + auditLoggingType: 'ecs', accessAgreementEnabled: false, authProviderCount: 1, enabledAuthProviders: ['basic'], diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts index 813e23a13ff37..5b8e9ef4f4f84 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -12,6 +12,7 @@ import type { ConfigType } from '../config'; interface Usage { auditLoggingEnabled: boolean; + auditLoggingType?: 'ecs' | 'legacy'; loginSelectorEnabled: boolean; accessAgreementEnabled: boolean; authProviderCount: number; @@ -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: { @@ -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 = [ @@ -140,7 +155,8 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens ); return { - auditLoggingEnabled: legacyAuditLoggingEnabled || auditLoggingEnabled, + auditLoggingEnabled: legacyAuditLoggingEnabled || ecsAuditLoggingEnabled, + auditLoggingType, loginSelectorEnabled, accessAgreementEnabled, authProviderCount, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 41af9b0754841..c76ba844fe071 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -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": { From 84234bd33b25657cc2feeadfcffa85669ce52b7e Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Aug 2021 18:19:10 -0400 Subject: [PATCH 02/10] ES username usage data --- .../core_usage_data_service.test.ts | 88 ++++++++++++++----- .../core_usage_data_service.ts | 18 ++++ src/core/server/core_usage_data/types.ts | 1 + src/core/server/server.api.md | 1 + .../collectors/core/core_usage_collector.ts | 4 + src/plugins/telemetry/schema/oss_plugins.json | 6 ++ 6 files changed, 97 insertions(+), 21 deletions(-) diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index 7ecfa37492242..8511fd965aa5f 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -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'; @@ -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; + const mockConfig = { unused_config: {}, elasticsearch: { username: 'kibana_system', password: 'changeme' }, @@ -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); }); @@ -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( @@ -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 { @@ -241,6 +250,7 @@ describe('CoreUsageDataService', () => { "truststoreConfigured": false, "verificationMode": "full", }, + "username": "none", }, "http": Object { "basePathConfigured": false, @@ -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', () => { diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index 7cf38dddc563e..4aa739b6b5656 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -29,6 +29,7 @@ import type { CoreUsageDataStart, CoreUsageDataSetup, ConfigUsageData, + CoreConfigUsageData, } from './types'; import { isConfigured } from './is_configured'; import { ElasticsearchServiceStart } from '../elasticsearch'; @@ -253,6 +254,7 @@ export class CoreUsageDataService implements CoreService Date: Mon, 30 Aug 2021 18:58:13 -0400 Subject: [PATCH 03/10] Fix mock --- src/core/server/core_usage_data/core_usage_data_service.mock.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index a03f79096004b..fad45b969e9b1 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -47,6 +47,7 @@ const createStartContractMock = () => { keystoreConfigured: false, truststoreConfigured: false, }, + username: 'none', }, http: { basePathConfigured: false, From bc1842aaf67a5374122fc74e6bd2e450797b1a08 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 31 Aug 2021 09:39:50 -0400 Subject: [PATCH 04/10] Change core ES usage collection --- .../core_usage_data_service.mock.ts | 2 +- .../core_usage_data_service.test.ts | 44 +++++++++++++------ .../core_usage_data_service.ts | 13 +++--- src/core/server/core_usage_data/types.ts | 8 +++- src/core/server/server.api.md | 2 +- .../collectors/core/core_usage_collector.ts | 7 ++- src/plugins/telemetry/schema/oss_plugins.json | 4 +- 7 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index fad45b969e9b1..4cc9a228ed4e4 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -47,7 +47,7 @@ const createStartContractMock = () => { keystoreConfigured: false, truststoreConfigured: false, }, - username: 'none', + principal: 'other', }, http: { basePathConfigured: false, diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index 8511fd965aa5f..28258cd1eb55a 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -235,6 +235,7 @@ describe('CoreUsageDataService', () => { "logQueries": false, "numberOfHostsConfigured": 1, "pingTimeoutMs": 30000, + "principal": "other", "requestHeadersWhitelistConfigured": false, "requestTimeoutMs": 30000, "shardTimeoutMs": 30000, @@ -250,7 +251,6 @@ describe('CoreUsageDataService', () => { "truststoreConfigured": false, "verificationMode": "full", }, - "username": "none", }, "http": Object { "basePathConfigured": false, @@ -365,12 +365,22 @@ describe('CoreUsageDataService', () => { `); }); - describe('elasticsearch.username', () => { - async function doTest({ username, expected }: { username: string; expected: string }) { + describe('elasticsearch.principal', () => { + async function doTest({ + username, + serviceAccountToken, + expectedPrincipal, + }: { + username?: string; + serviceAccountToken?: string; + expectedPrincipal: string; + }) { const defaultMockImplementation = getConfigServiceAtPathMockImplementation(); configService.atPath.mockImplementation((path) => { if (path === 'elasticsearch') { - return new BehaviorSubject(RawElasticsearchConfig.schema.validate({ username })); + return new BehaviorSubject( + RawElasticsearchConfig.schema.validate({ username, serviceAccountToken }) + ); } return defaultMockImplementation(path); }); @@ -378,26 +388,34 @@ describe('CoreUsageDataService', () => { return expect(getCoreUsageData()).resolves.toEqual( expect.objectContaining({ config: expect.objectContaining({ - elasticsearch: expect.objectContaining({ username: expected }), + elasticsearch: expect.objectContaining({ principal: expectedPrincipal }), }), }) ); } - it('returns expected usage data for "elastic"', async () => { - return doTest({ username: 'elastic', expected: 'elastic' }); + it('returns expected usage data for elastic.username "elastic"', async () => { + return doTest({ username: 'elastic', expectedPrincipal: 'elastic_user' }); }); - it('returns expected usage data for "kibana"', async () => { - return doTest({ username: 'kibana', expected: 'kibana' }); + it('returns expected usage data for elastic.username "kibana"', async () => { + return doTest({ username: 'kibana', expectedPrincipal: 'kibana_user' }); }); - it('returns expected usage data for "kibana_system"', async () => { - return doTest({ username: 'kibana_system', expected: 'kibana_system' }); + it('returns expected usage data for elastic.username "kibana_system"', async () => { + return doTest({ username: 'kibana_system', expectedPrincipal: 'kibana_system_user' }); }); - it('returns expected usage data for anything else', async () => { - return doTest({ username: 'anything else', expected: 'other' }); + it('returns expected usage data for elastic.username anything else', async () => { + return doTest({ username: 'anything else', expectedPrincipal: 'other_user' }); + }); + + it('returns expected usage data for elastic.serviceAccountToken', async () => { + // Note: elastic.username and elastic.serviceAccountToken are mutually exclusive + return doTest({ + serviceAccountToken: 'any', + expectedPrincipal: 'kibana_service_account', + }); }); }); }); diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index 4aa739b6b5656..92464b6848077 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -254,7 +254,7 @@ export class CoreUsageDataService implements CoreService Date: Tue, 31 Aug 2021 12:33:00 -0400 Subject: [PATCH 05/10] Add session timeout usage data --- x-pack/plugins/security/server/config.ts | 11 +- .../security_usage_collector.test.ts | 109 ++++++++---------- .../security_usage_collector.ts | 24 ++++ .../schema/xpack_plugins.json | 12 ++ 4 files changed, 94 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 6ce161a898810..40b1b9d10b801 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -391,11 +391,18 @@ export function createConfig( function getSessionConfig(session: RawConfigType['session'], providers: ProvidersConfigType) { return { cleanupInterval: session.cleanupInterval, - getExpirationTimeouts({ type, name }: AuthenticationProvider) { + getExpirationTimeouts(provider?: AuthenticationProvider) { // Both idle timeout and lifespan from the provider specific session config can have three // possible types of values: `Duration`, `null` and `undefined`. The `undefined` type means that // provider doesn't override session config and we should fall back to the global one instead. - const providerSessionConfig = providers[type as keyof ProvidersConfigType]?.[name]?.session; + // Note: using an `undefined` provider argument returns the global timeouts. + let providerSessionConfig: + | { idleTimeout?: Duration | null; lifespan?: Duration | null } + | undefined; + if (provider) { + const { type, name } = provider; + providerSessionConfig = providers[type as keyof ProvidersConfigType]?.[name]?.session; + } const [idleTimeout, lifespan] = [ [session.idleTimeout, providerSessionConfig?.idleTimeout], [session.lifespan, providerSessionConfig?.lifespan], diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts index c02e644329805..7d50e05398c09 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts @@ -40,6 +40,16 @@ describe('Security UsageCollector', () => { }; const collectorFetchContext = createCollectorFetchContextMock(); + const DEFAULT_USAGE = { + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 1, + enabledAuthProviders: ['basic'], + loginSelectorEnabled: false, + httpAuthSchemes: ['apikey'], + sessionIdleTimeoutMinutes: 60, + sessionLifespanMinutes: 43200, + }; describe('initialization', () => { it('handles an undefined usage collector', () => { @@ -75,14 +85,7 @@ describe('Security UsageCollector', () => { .getCollectorByType('security') ?.fetch(collectorFetchContext); - expect(usage).toEqual({ - auditLoggingEnabled: false, - accessAgreementEnabled: false, - authProviderCount: 1, - enabledAuthProviders: ['basic'], - loginSelectorEnabled: false, - httpAuthSchemes: ['apikey'], - }); + expect(usage).toEqual(DEFAULT_USAGE); }); it('reports correctly when security is disabled in Elasticsearch', async () => { @@ -103,6 +106,8 @@ describe('Security UsageCollector', () => { enabledAuthProviders: [], loginSelectorEnabled: false, httpAuthSchemes: [], + sessionIdleTimeoutMinutes: 0, + sessionLifespanMinutes: 0, }); }); @@ -140,14 +145,7 @@ describe('Security UsageCollector', () => { .getCollectorByType('security') ?.fetch(collectorFetchContext); - expect(usage).toEqual({ - auditLoggingEnabled: false, - accessAgreementEnabled: false, - authProviderCount: 1, - enabledAuthProviders: ['basic'], - loginSelectorEnabled: false, - httpAuthSchemes: ['apikey'], - }); + expect(usage).toEqual(DEFAULT_USAGE); }); it('reports the types and count of enabled auth providers', async () => { @@ -190,12 +188,10 @@ describe('Security UsageCollector', () => { ?.fetch(collectorFetchContext); expect(usage).toEqual({ - auditLoggingEnabled: false, - accessAgreementEnabled: false, + ...DEFAULT_USAGE, authProviderCount: 3, enabledAuthProviders: ['saml', 'pki'], loginSelectorEnabled: true, - httpAuthSchemes: ['apikey'], }); }); }); @@ -228,12 +224,9 @@ describe('Security UsageCollector', () => { ?.fetch(collectorFetchContext); expect(usage).toEqual({ - auditLoggingEnabled: false, + ...DEFAULT_USAGE, accessAgreementEnabled: true, - authProviderCount: 1, enabledAuthProviders: ['saml'], - loginSelectorEnabled: false, - httpAuthSchemes: ['apikey'], }); }); it('does not report the access agreement if the license does not permit it', async () => { @@ -266,12 +259,9 @@ describe('Security UsageCollector', () => { ?.fetch(collectorFetchContext); expect(usage).toEqual({ - auditLoggingEnabled: false, + ...DEFAULT_USAGE, accessAgreementEnabled: false, - authProviderCount: 1, enabledAuthProviders: ['saml'], - loginSelectorEnabled: false, - httpAuthSchemes: ['apikey'], }); }); @@ -307,12 +297,9 @@ describe('Security UsageCollector', () => { ?.fetch(collectorFetchContext); expect(usage).toEqual({ - auditLoggingEnabled: false, + ...DEFAULT_USAGE, accessAgreementEnabled: false, - authProviderCount: 1, enabledAuthProviders: ['saml'], - loginSelectorEnabled: false, - httpAuthSchemes: ['apikey'], }); }); }); @@ -346,12 +333,9 @@ describe('Security UsageCollector', () => { ?.fetch(collectorFetchContext); expect(usage).toEqual({ - auditLoggingEnabled: false, - accessAgreementEnabled: false, - authProviderCount: 1, + ...DEFAULT_USAGE, enabledAuthProviders: ['saml'], loginSelectorEnabled: true, - httpAuthSchemes: ['apikey'], }); }); }); @@ -379,13 +363,9 @@ describe('Security UsageCollector', () => { ?.fetch(collectorFetchContext); expect(usage).toEqual({ + ...DEFAULT_USAGE, auditLoggingEnabled: true, auditLoggingType: 'legacy', - accessAgreementEnabled: false, - authProviderCount: 1, - enabledAuthProviders: ['basic'], - loginSelectorEnabled: false, - httpAuthSchemes: ['apikey'], }); }); @@ -411,13 +391,9 @@ describe('Security UsageCollector', () => { ?.fetch(collectorFetchContext); expect(usage).toEqual({ + ...DEFAULT_USAGE, auditLoggingEnabled: true, auditLoggingType: 'ecs', - accessAgreementEnabled: false, - authProviderCount: 1, - enabledAuthProviders: ['basic'], - loginSelectorEnabled: false, - httpAuthSchemes: ['apikey'], }); }); @@ -438,12 +414,9 @@ describe('Security UsageCollector', () => { ?.fetch(collectorFetchContext); expect(usage).toEqual({ + ...DEFAULT_USAGE, auditLoggingEnabled: false, - accessAgreementEnabled: false, - authProviderCount: 1, - enabledAuthProviders: ['basic'], - loginSelectorEnabled: false, - httpAuthSchemes: ['apikey'], + auditLoggingType: undefined, }); }); }); @@ -468,11 +441,7 @@ describe('Security UsageCollector', () => { ?.fetch(collectorFetchContext); expect(usage).toEqual({ - auditLoggingEnabled: false, - accessAgreementEnabled: false, - authProviderCount: 1, - enabledAuthProviders: ['basic'], - loginSelectorEnabled: false, + ...DEFAULT_USAGE, httpAuthSchemes: ['basic', 'Negotiate'], }); }); @@ -496,13 +465,33 @@ describe('Security UsageCollector', () => { ?.fetch(collectorFetchContext); expect(usage).toEqual({ - auditLoggingEnabled: false, - accessAgreementEnabled: false, - authProviderCount: 1, - enabledAuthProviders: ['basic'], - loginSelectorEnabled: false, + ...DEFAULT_USAGE, httpAuthSchemes: ['basic', 'Negotiate'], }); }); }); + + describe('session expirations', () => { + // Note: can't easily test deprecated 'sessionTimeout' value here because of the way that config deprecation renaming works + it('reports customized session idleTimeout and lifespan', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + session: { idleTimeout: '123m', lifespan: '456m' }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(collectorFetchContext); + + expect(usage).toEqual({ + ...DEFAULT_USAGE, + sessionIdleTimeoutMinutes: 123, + sessionLifespanMinutes: 456, + }); + }); + }); }); diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts index 5b8e9ef4f4f84..fa79eeda58bcd 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -18,6 +18,8 @@ interface Usage { authProviderCount: number; enabledAuthProviders: string[]; httpAuthSchemes: string[]; + sessionIdleTimeoutMinutes: number; + sessionLifespanMinutes: number; } interface Deps { @@ -106,6 +108,20 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens }, }, }, + sessionIdleTimeoutMinutes: { + type: 'long', + _meta: { + description: + 'The global session idle timeout expiration that is configured, in minutes (0 if disabled).', + }, + }, + sessionLifespanMinutes: { + type: 'long', + _meta: { + description: + 'The global session lifespan expiration that is configured, in minutes (0 if disabled).', + }, + }, }, fetch: () => { const { @@ -122,6 +138,8 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens authProviderCount: 0, enabledAuthProviders: [], httpAuthSchemes: [], + sessionIdleTimeoutMinutes: 0, + sessionLifespanMinutes: 0, }; } @@ -154,6 +172,10 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens WELL_KNOWN_AUTH_SCHEMES.includes(scheme.toLowerCase()) ); + const sessionExpirations = config.session.getExpirationTimeouts(); // get global expiration values + const sessionIdleTimeoutMinutes = sessionExpirations.idleTimeout?.asMinutes() ?? 0; + const sessionLifespanMinutes = sessionExpirations.lifespan?.asMinutes() ?? 0; + return { auditLoggingEnabled: legacyAuditLoggingEnabled || ecsAuditLoggingEnabled, auditLoggingType, @@ -162,6 +184,8 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens authProviderCount, enabledAuthProviders, httpAuthSchemes, + sessionIdleTimeoutMinutes, + sessionLifespanMinutes, }; }, }); diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index c76ba844fe071..c0bfe26d05914 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -5496,6 +5496,18 @@ "description": "The set of enabled http auth schemes. Used for api-based usage, and when credentials are provided via reverse-proxy." } } + }, + "sessionIdleTimeoutMinutes": { + "type": "long", + "_meta": { + "description": "The global session idle timeout expiration that is configured, in minutes (0 if disabled)." + } + }, + "sessionLifespanMinutes": { + "type": "long", + "_meta": { + "description": "The global session lifespan expiration that is configured, in minutes (0 if disabled)." + } } } }, From fb0850b3250ad7692870594d5a2e5aadb6ff6e5e Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 31 Aug 2021 12:42:28 -0400 Subject: [PATCH 06/10] Rename session expiration usage data --- .../security_usage_collector.test.ts | 12 +++++------ .../security_usage_collector.ts | 20 +++++++++---------- .../schema/xpack_plugins.json | 4 ++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts index 7d50e05398c09..c9e1cf6915bba 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts @@ -47,8 +47,8 @@ describe('Security UsageCollector', () => { enabledAuthProviders: ['basic'], loginSelectorEnabled: false, httpAuthSchemes: ['apikey'], - sessionIdleTimeoutMinutes: 60, - sessionLifespanMinutes: 43200, + sessionIdleTimeoutInMinutes: 60, + sessionLifespanInMinutes: 43200, }; describe('initialization', () => { @@ -106,8 +106,8 @@ describe('Security UsageCollector', () => { enabledAuthProviders: [], loginSelectorEnabled: false, httpAuthSchemes: [], - sessionIdleTimeoutMinutes: 0, - sessionLifespanMinutes: 0, + sessionIdleTimeoutInMinutes: 0, + sessionLifespanInMinutes: 0, }); }); @@ -489,8 +489,8 @@ describe('Security UsageCollector', () => { expect(usage).toEqual({ ...DEFAULT_USAGE, - sessionIdleTimeoutMinutes: 123, - sessionLifespanMinutes: 456, + sessionIdleTimeoutInMinutes: 123, + sessionLifespanInMinutes: 456, }); }); }); diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts index fa79eeda58bcd..5703969c4df79 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -18,8 +18,8 @@ interface Usage { authProviderCount: number; enabledAuthProviders: string[]; httpAuthSchemes: string[]; - sessionIdleTimeoutMinutes: number; - sessionLifespanMinutes: number; + sessionIdleTimeoutInMinutes: number; + sessionLifespanInMinutes: number; } interface Deps { @@ -108,14 +108,14 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens }, }, }, - sessionIdleTimeoutMinutes: { + sessionIdleTimeoutInMinutes: { type: 'long', _meta: { description: 'The global session idle timeout expiration that is configured, in minutes (0 if disabled).', }, }, - sessionLifespanMinutes: { + sessionLifespanInMinutes: { type: 'long', _meta: { description: @@ -138,8 +138,8 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens authProviderCount: 0, enabledAuthProviders: [], httpAuthSchemes: [], - sessionIdleTimeoutMinutes: 0, - sessionLifespanMinutes: 0, + sessionIdleTimeoutInMinutes: 0, + sessionLifespanInMinutes: 0, }; } @@ -173,8 +173,8 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens ); const sessionExpirations = config.session.getExpirationTimeouts(); // get global expiration values - const sessionIdleTimeoutMinutes = sessionExpirations.idleTimeout?.asMinutes() ?? 0; - const sessionLifespanMinutes = sessionExpirations.lifespan?.asMinutes() ?? 0; + const sessionIdleTimeoutInMinutes = sessionExpirations.idleTimeout?.asMinutes() ?? 0; + const sessionLifespanInMinutes = sessionExpirations.lifespan?.asMinutes() ?? 0; return { auditLoggingEnabled: legacyAuditLoggingEnabled || ecsAuditLoggingEnabled, @@ -184,8 +184,8 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens authProviderCount, enabledAuthProviders, httpAuthSchemes, - sessionIdleTimeoutMinutes, - sessionLifespanMinutes, + sessionIdleTimeoutInMinutes, + sessionLifespanInMinutes, }; }, }); diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index c0bfe26d05914..10c84ecc06d5a 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -5497,13 +5497,13 @@ } } }, - "sessionIdleTimeoutMinutes": { + "sessionIdleTimeoutInMinutes": { "type": "long", "_meta": { "description": "The global session idle timeout expiration that is configured, in minutes (0 if disabled)." } }, - "sessionLifespanMinutes": { + "sessionLifespanInMinutes": { "type": "long", "_meta": { "description": "The global session lifespan expiration that is configured, in minutes (0 if disabled)." From 94379a67015a453fee806e2c2e2f55fa1d515c1b Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 31 Aug 2021 12:46:59 -0400 Subject: [PATCH 07/10] Add session cleanup usage data --- .../usage_collector/security_usage_collector.test.ts | 9 ++++++--- .../usage_collector/security_usage_collector.ts | 11 +++++++++++ .../schema/xpack_plugins.json | 6 ++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts index c9e1cf6915bba..0515a1e1969bf 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts @@ -49,6 +49,7 @@ describe('Security UsageCollector', () => { httpAuthSchemes: ['apikey'], sessionIdleTimeoutInMinutes: 60, sessionLifespanInMinutes: 43200, + sessionCleanupInMinutes: 60, }; describe('initialization', () => { @@ -108,6 +109,7 @@ describe('Security UsageCollector', () => { httpAuthSchemes: [], sessionIdleTimeoutInMinutes: 0, sessionLifespanInMinutes: 0, + sessionCleanupInMinutes: 0, }); }); @@ -471,12 +473,12 @@ describe('Security UsageCollector', () => { }); }); - describe('session expirations', () => { + describe('session', () => { // Note: can't easily test deprecated 'sessionTimeout' value here because of the way that config deprecation renaming works - it('reports customized session idleTimeout and lifespan', async () => { + it('reports customized session idleTimeout, lifespan, and cleanupInterval', async () => { const config = createSecurityConfig( ConfigSchema.validate({ - session: { idleTimeout: '123m', lifespan: '456m' }, + session: { idleTimeout: '123m', lifespan: '456m', cleanupInterval: '789m' }, }) ); const usageCollection = usageCollectionPluginMock.createSetupContract(); @@ -491,6 +493,7 @@ describe('Security UsageCollector', () => { ...DEFAULT_USAGE, sessionIdleTimeoutInMinutes: 123, sessionLifespanInMinutes: 456, + sessionCleanupInMinutes: 789, }); }); }); diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts index 5703969c4df79..b09866ee47d2e 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -20,6 +20,7 @@ interface Usage { httpAuthSchemes: string[]; sessionIdleTimeoutInMinutes: number; sessionLifespanInMinutes: number; + sessionCleanupInMinutes: number; } interface Deps { @@ -122,6 +123,13 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens 'The global session lifespan expiration that is configured, in minutes (0 if disabled).', }, }, + sessionCleanupInMinutes: { + type: 'long', + _meta: { + description: + 'The session cleanup interval that is configured, in minutes (0 if disabled).', + }, + }, }, fetch: () => { const { @@ -140,6 +148,7 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens httpAuthSchemes: [], sessionIdleTimeoutInMinutes: 0, sessionLifespanInMinutes: 0, + sessionCleanupInMinutes: 0, }; } @@ -175,6 +184,7 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens const sessionExpirations = config.session.getExpirationTimeouts(); // get global expiration values const sessionIdleTimeoutInMinutes = sessionExpirations.idleTimeout?.asMinutes() ?? 0; const sessionLifespanInMinutes = sessionExpirations.lifespan?.asMinutes() ?? 0; + const sessionCleanupInMinutes = config.session.cleanupInterval?.asMinutes() ?? 0; return { auditLoggingEnabled: legacyAuditLoggingEnabled || ecsAuditLoggingEnabled, @@ -186,6 +196,7 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens httpAuthSchemes, sessionIdleTimeoutInMinutes, sessionLifespanInMinutes, + sessionCleanupInMinutes, }; }, }); diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 10c84ecc06d5a..38e74e15f7ae7 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -5508,6 +5508,12 @@ "_meta": { "description": "The global session lifespan expiration that is configured, in minutes (0 if disabled)." } + }, + "sessionCleanupInMinutes": { + "type": "long", + "_meta": { + "description": "The session cleanup interval that is configured, in minutes (0 if disabled)." + } } } }, From a9959d1cc498ffb82a3c386d3b133212d25bad05 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:12:16 -0400 Subject: [PATCH 08/10] Tweak security config --- x-pack/plugins/security/server/config.test.ts | 64 +++++++++---------- x-pack/plugins/security/server/config.ts | 2 +- .../security_usage_collector.ts | 2 +- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 4593d9a7ad682..75dfcb6151ea7 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -1689,41 +1689,39 @@ describe('createConfig()', () => { `); }); - it('falls back to the global settings if provider is not known', async () => { - expect( - createMockConfig({ session: { idleTimeout: 123 } }).session.getExpirationTimeouts({ - type: 'some type', - name: 'some name', - }) - ).toMatchInlineSnapshot(` - Object { - "idleTimeout": "PT0.123S", - "lifespan": "P30D", - } - `); + it('falls back to the global settings if provider is not known or is undefined', async () => { + [{ type: 'some type', name: 'some name' }, undefined].forEach((provider) => { + expect( + createMockConfig({ session: { idleTimeout: 123 } }).session.getExpirationTimeouts( + provider + ) + ).toMatchInlineSnapshot(` + Object { + "idleTimeout": "PT0.123S", + "lifespan": "P30D", + } + `); - expect( - createMockConfig({ session: { lifespan: 456 } }).session.getExpirationTimeouts({ - type: 'some type', - name: 'some name', - }) - ).toMatchInlineSnapshot(` - Object { - "idleTimeout": "PT1H", - "lifespan": "PT0.456S", - } - `); + expect( + createMockConfig({ session: { lifespan: 456 } }).session.getExpirationTimeouts(provider) + ).toMatchInlineSnapshot(` + Object { + "idleTimeout": "PT1H", + "lifespan": "PT0.456S", + } + `); - expect( - createMockConfig({ - session: { idleTimeout: 123, lifespan: 456 }, - }).session.getExpirationTimeouts({ type: 'some type', name: 'some name' }) - ).toMatchInlineSnapshot(` - Object { - "idleTimeout": "PT0.123S", - "lifespan": "PT0.456S", - } - `); + expect( + createMockConfig({ + session: { idleTimeout: 123, lifespan: 456 }, + }).session.getExpirationTimeouts(provider) + ).toMatchInlineSnapshot(` + Object { + "idleTimeout": "PT0.123S", + "lifespan": "PT0.456S", + } + `); + }); }); it('uses provider overrides if specified (only idle timeout)', async () => { diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 40b1b9d10b801..9daf0aff4c6cb 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -391,7 +391,7 @@ export function createConfig( function getSessionConfig(session: RawConfigType['session'], providers: ProvidersConfigType) { return { cleanupInterval: session.cleanupInterval, - getExpirationTimeouts(provider?: AuthenticationProvider) { + getExpirationTimeouts(provider: AuthenticationProvider | undefined) { // Both idle timeout and lifespan from the provider specific session config can have three // possible types of values: `Duration`, `null` and `undefined`. The `undefined` type means that // provider doesn't override session config and we should fall back to the global one instead. diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts index b09866ee47d2e..15177132e0fb1 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -181,7 +181,7 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens WELL_KNOWN_AUTH_SCHEMES.includes(scheme.toLowerCase()) ); - const sessionExpirations = config.session.getExpirationTimeouts(); // get global expiration values + const sessionExpirations = config.session.getExpirationTimeouts(undefined); // use `undefined` to get global expiration values const sessionIdleTimeoutInMinutes = sessionExpirations.idleTimeout?.asMinutes() ?? 0; const sessionLifespanInMinutes = sessionExpirations.lifespan?.asMinutes() ?? 0; const sessionCleanupInMinutes = config.session.cleanupInterval?.asMinutes() ?? 0; From 0317d339cbf1c4ef29692e17f8259d3e44cf7ee5 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:16:27 -0400 Subject: [PATCH 09/10] Change elasticsearch.principal usage data/description --- src/core/server/core_usage_data/core_usage_data_service.test.ts | 2 +- src/core/server/core_usage_data/core_usage_data_service.ts | 2 +- src/core/server/core_usage_data/types.ts | 2 +- src/core/server/server.api.md | 2 +- .../server/collectors/core/core_usage_collector.ts | 2 +- src/plugins/telemetry/schema/oss_plugins.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index 28258cd1eb55a..478cfe5daff46 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -235,7 +235,7 @@ describe('CoreUsageDataService', () => { "logQueries": false, "numberOfHostsConfigured": 1, "pingTimeoutMs": 30000, - "principal": "other", + "principal": "unknown", "requestHeadersWhitelistConfigured": false, "requestTimeoutMs": 30000, "shardTimeoutMs": 30000, diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index 92464b6848077..73f63d4d634df 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -516,7 +516,7 @@ export class CoreUsageDataService implements CoreService Date: Tue, 31 Aug 2021 16:35:31 -0400 Subject: [PATCH 10/10] Fix type check --- src/core/server/core_usage_data/core_usage_data_service.mock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index 4cc9a228ed4e4..941ac5afacb40 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -47,7 +47,7 @@ const createStartContractMock = () => { keystoreConfigured: false, truststoreConfigured: false, }, - principal: 'other', + principal: 'unknown', }, http: { basePathConfigured: false,