Security usage data (#110548)
This commit is contained in:
parent
77890b1ccf
commit
b17d87e508
|
@ -47,6 +47,7 @@ const createStartContractMock = () => {
|
|||
keystoreConfigured: false,
|
||||
truststoreConfigured: false,
|
||||
},
|
||||
principal: 'unknown',
|
||||
},
|
||||
http: {
|
||||
basePathConfigured: false,
|
||||
|
|
|
@ -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<typeof configServiceMock.create>;
|
||||
|
||||
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 {
|
||||
|
@ -226,6 +235,7 @@ describe('CoreUsageDataService', () => {
|
|||
"logQueries": false,
|
||||
"numberOfHostsConfigured": 1,
|
||||
"pingTimeoutMs": 30000,
|
||||
"principal": "unknown",
|
||||
"requestHeadersWhitelistConfigured": false,
|
||||
"requestTimeoutMs": 30000,
|
||||
"shardTimeoutMs": 30000,
|
||||
|
@ -354,6 +364,60 @@ describe('CoreUsageDataService', () => {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
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, serviceAccountToken })
|
||||
);
|
||||
}
|
||||
return defaultMockImplementation(path);
|
||||
});
|
||||
const { getCoreUsageData } = setup();
|
||||
return expect(getCoreUsageData()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
elasticsearch: expect.objectContaining({ principal: expectedPrincipal }),
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
it('returns expected usage data for elastic.username "elastic"', async () => {
|
||||
return doTest({ username: 'elastic', expectedPrincipal: 'elastic_user' });
|
||||
});
|
||||
|
||||
it('returns expected usage data for elastic.username "kibana"', async () => {
|
||||
return doTest({ username: 'kibana', expectedPrincipal: 'kibana_user' });
|
||||
});
|
||||
|
||||
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 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfigsUsageData', () => {
|
||||
|
|
|
@ -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<CoreUsageDataSetup, Cor
|
|||
truststoreConfigured: isConfigured.record(es.ssl.truststore),
|
||||
keystoreConfigured: isConfigured.record(es.ssl.keystore),
|
||||
},
|
||||
principal: getEsPrincipalUsage(es),
|
||||
},
|
||||
http: {
|
||||
basePathConfigured: isConfigured.string(http.basePath),
|
||||
|
@ -512,3 +514,22 @@ export class CoreUsageDataService implements CoreService<CoreUsageDataSetup, Cor
|
|||
this.stop$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
function getEsPrincipalUsage({ username, serviceAccountToken }: ElasticsearchConfigType) {
|
||||
let value: CoreConfigUsageData['elasticsearch']['principal'] = 'unknown';
|
||||
if (isConfigured.string(username)) {
|
||||
switch (username) {
|
||||
case 'elastic': // deprecated
|
||||
case 'kibana': // deprecated
|
||||
case 'kibana_system':
|
||||
value = `${username}_user` as const;
|
||||
break;
|
||||
default:
|
||||
value = 'other_user';
|
||||
}
|
||||
} else if (serviceAccountToken) {
|
||||
// cannot be used with elasticsearch.username
|
||||
value = 'kibana_service_account';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
|
|
@ -205,6 +205,13 @@ export interface CoreConfigUsageData {
|
|||
};
|
||||
apiVersion: string;
|
||||
healthCheckDelayMs: number;
|
||||
principal:
|
||||
| 'elastic_user'
|
||||
| 'kibana_user'
|
||||
| 'kibana_system_user'
|
||||
| 'other_user'
|
||||
| 'kibana_service_account'
|
||||
| 'unknown';
|
||||
};
|
||||
|
||||
http: {
|
||||
|
|
|
@ -293,6 +293,7 @@ export interface CoreConfigUsageData {
|
|||
};
|
||||
apiVersion: string;
|
||||
healthCheckDelayMs: number;
|
||||
principal: 'elastic_user' | 'kibana_user' | 'kibana_system_user' | 'other_user' | 'kibana_service_account' | 'unknown';
|
||||
};
|
||||
// (undocumented)
|
||||
http: {
|
||||
|
|
|
@ -131,6 +131,13 @@ export function getCoreUsageCollector(
|
|||
'The interval in miliseconds between health check requests Kibana sends to the Elasticsearch.',
|
||||
},
|
||||
},
|
||||
principal: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description:
|
||||
'Indicates how Kibana authenticates itself to Elasticsearch. If elasticsearch.username is configured, this can be any of: "elastic_user", "kibana_user", "kibana_system_user", or "other_user". Otherwise, if elasticsearch.serviceAccountToken is configured, this will be "kibana_service_account". Otherwise, this value will be "unknown", because some other principal might be used to authenticate Kibana to Elasticsearch (such as an x509 certificate), or authentication may be skipped altogether.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
http: {
|
||||
|
|
|
@ -5957,6 +5957,12 @@
|
|||
"_meta": {
|
||||
"description": "The interval in miliseconds between health check requests Kibana sends to the Elasticsearch."
|
||||
}
|
||||
},
|
||||
"principal": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "Indicates how Kibana authenticates itself to Elasticsearch. If elasticsearch.username is configured, this can be any of: \"elastic_user\", \"kibana_user\", \"kibana_system_user\", or \"other_user\". Otherwise, if elasticsearch.serviceAccountToken is configured, this will be \"kibana_service_account\". Otherwise, this value will be \"unknown\", because some other principal might be used to authenticate Kibana to Elasticsearch (such as an x509 certificate), or authentication may be skipped altogether."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 | 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.
|
||||
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],
|
||||
|
|
|
@ -40,6 +40,17 @@ describe('Security UsageCollector', () => {
|
|||
};
|
||||
|
||||
const collectorFetchContext = createCollectorFetchContextMock();
|
||||
const DEFAULT_USAGE = {
|
||||
auditLoggingEnabled: false,
|
||||
accessAgreementEnabled: false,
|
||||
authProviderCount: 1,
|
||||
enabledAuthProviders: ['basic'],
|
||||
loginSelectorEnabled: false,
|
||||
httpAuthSchemes: ['apikey'],
|
||||
sessionIdleTimeoutInMinutes: 60,
|
||||
sessionLifespanInMinutes: 43200,
|
||||
sessionCleanupInMinutes: 60,
|
||||
};
|
||||
|
||||
describe('initialization', () => {
|
||||
it('handles an undefined usage collector', () => {
|
||||
|
@ -75,14 +86,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 +107,9 @@ describe('Security UsageCollector', () => {
|
|||
enabledAuthProviders: [],
|
||||
loginSelectorEnabled: false,
|
||||
httpAuthSchemes: [],
|
||||
sessionIdleTimeoutInMinutes: 0,
|
||||
sessionLifespanInMinutes: 0,
|
||||
sessionCleanupInMinutes: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -140,14 +147,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 +190,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 +226,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 +261,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 +299,9 @@ describe('Security UsageCollector', () => {
|
|||
?.fetch(collectorFetchContext);
|
||||
|
||||
expect(usage).toEqual({
|
||||
auditLoggingEnabled: false,
|
||||
...DEFAULT_USAGE,
|
||||
accessAgreementEnabled: false,
|
||||
authProviderCount: 1,
|
||||
enabledAuthProviders: ['saml'],
|
||||
loginSelectorEnabled: false,
|
||||
httpAuthSchemes: ['apikey'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -346,27 +335,29 @@ describe('Security UsageCollector', () => {
|
|||
?.fetch(collectorFetchContext);
|
||||
|
||||
expect(usage).toEqual({
|
||||
auditLoggingEnabled: false,
|
||||
accessAgreementEnabled: false,
|
||||
authProviderCount: 1,
|
||||
...DEFAULT_USAGE,
|
||||
enabledAuthProviders: ['saml'],
|
||||
loginSelectorEnabled: true,
|
||||
httpAuthSchemes: ['apikey'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
|
@ -374,12 +365,37 @@ describe('Security UsageCollector', () => {
|
|||
?.fetch(collectorFetchContext);
|
||||
|
||||
expect(usage).toEqual({
|
||||
...DEFAULT_USAGE,
|
||||
auditLoggingEnabled: true,
|
||||
accessAgreementEnabled: false,
|
||||
authProviderCount: 1,
|
||||
enabledAuthProviders: ['basic'],
|
||||
loginSelectorEnabled: false,
|
||||
httpAuthSchemes: ['apikey'],
|
||||
auditLoggingType: 'legacy',
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
.getCollectorByType('security')
|
||||
?.fetch(collectorFetchContext);
|
||||
|
||||
expect(usage).toEqual({
|
||||
...DEFAULT_USAGE,
|
||||
auditLoggingEnabled: true,
|
||||
auditLoggingType: 'ecs',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -400,12 +416,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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -430,11 +443,7 @@ describe('Security UsageCollector', () => {
|
|||
?.fetch(collectorFetchContext);
|
||||
|
||||
expect(usage).toEqual({
|
||||
auditLoggingEnabled: false,
|
||||
accessAgreementEnabled: false,
|
||||
authProviderCount: 1,
|
||||
enabledAuthProviders: ['basic'],
|
||||
loginSelectorEnabled: false,
|
||||
...DEFAULT_USAGE,
|
||||
httpAuthSchemes: ['basic', 'Negotiate'],
|
||||
});
|
||||
});
|
||||
|
@ -458,13 +467,34 @@ 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', () => {
|
||||
// Note: can't easily test deprecated 'sessionTimeout' value here because of the way that config deprecation renaming works
|
||||
it('reports customized session idleTimeout, lifespan, and cleanupInterval', async () => {
|
||||
const config = createSecurityConfig(
|
||||
ConfigSchema.validate({
|
||||
session: { idleTimeout: '123m', lifespan: '456m', cleanupInterval: '789m' },
|
||||
})
|
||||
);
|
||||
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,
|
||||
sessionIdleTimeoutInMinutes: 123,
|
||||
sessionLifespanInMinutes: 456,
|
||||
sessionCleanupInMinutes: 789,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,11 +12,15 @@ import type { ConfigType } from '../config';
|
|||
|
||||
interface Usage {
|
||||
auditLoggingEnabled: boolean;
|
||||
auditLoggingType?: 'ecs' | 'legacy';
|
||||
loginSelectorEnabled: boolean;
|
||||
accessAgreementEnabled: boolean;
|
||||
authProviderCount: number;
|
||||
enabledAuthProviders: string[];
|
||||
httpAuthSchemes: string[];
|
||||
sessionIdleTimeoutInMinutes: number;
|
||||
sessionLifespanInMinutes: number;
|
||||
sessionCleanupInMinutes: number;
|
||||
}
|
||||
|
||||
interface Deps {
|
||||
|
@ -58,6 +62,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: {
|
||||
|
@ -98,6 +109,27 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens
|
|||
},
|
||||
},
|
||||
},
|
||||
sessionIdleTimeoutInMinutes: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description:
|
||||
'The global session idle timeout expiration that is configured, in minutes (0 if disabled).',
|
||||
},
|
||||
},
|
||||
sessionLifespanInMinutes: {
|
||||
type: 'long',
|
||||
_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).',
|
||||
},
|
||||
},
|
||||
},
|
||||
fetch: () => {
|
||||
const {
|
||||
|
@ -114,13 +146,23 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens
|
|||
authProviderCount: 0,
|
||||
enabledAuthProviders: [],
|
||||
httpAuthSchemes: [],
|
||||
sessionIdleTimeoutInMinutes: 0,
|
||||
sessionLifespanInMinutes: 0,
|
||||
sessionCleanupInMinutes: 0,
|
||||
};
|
||||
}
|
||||
|
||||
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 = [
|
||||
|
@ -139,13 +181,22 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens
|
|||
WELL_KNOWN_AUTH_SCHEMES.includes(scheme.toLowerCase())
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
return {
|
||||
auditLoggingEnabled: legacyAuditLoggingEnabled || auditLoggingEnabled,
|
||||
auditLoggingEnabled: legacyAuditLoggingEnabled || ecsAuditLoggingEnabled,
|
||||
auditLoggingType,
|
||||
loginSelectorEnabled,
|
||||
accessAgreementEnabled,
|
||||
authProviderCount,
|
||||
enabledAuthProviders,
|
||||
httpAuthSchemes,
|
||||
sessionIdleTimeoutInMinutes,
|
||||
sessionLifespanInMinutes,
|
||||
sessionCleanupInMinutes,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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": {
|
||||
|
@ -5490,6 +5496,24 @@
|
|||
"description": "The set of enabled http auth schemes. Used for api-based usage, and when credentials are provided via reverse-proxy."
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionIdleTimeoutInMinutes": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "The global session idle timeout expiration that is configured, in minutes (0 if disabled)."
|
||||
}
|
||||
},
|
||||
"sessionLifespanInMinutes": {
|
||||
"type": "long",
|
||||
"_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)."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue