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] add telemetry on authentication type #119371

Closed
wants to merge 12 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import { i18n } from '@kbn/i18n';
import type { CoreSetup, HttpSetup } from 'src/core/public';

import { SecurityTelemetryService } from '../../telemetry';
XavierM marked this conversation as resolved.
Show resolved Hide resolved

interface CreateDeps {
application: CoreSetup['application'];
http: HttpSetup;
Expand All @@ -24,6 +26,7 @@ export const logoutApp = Object.freeze({
appRoute: '/logout',
async mount() {
window.sessionStorage.clear();
window.localStorage.removeItem(SecurityTelemetryService.KeyAuthType);

// Redirect user to the server logout endpoint to complete logout.
window.location.href = http.basePath.prepend(
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/security/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { SecurityNavControlServiceStart } from './nav_control';
import { SecurityNavControlService } from './nav_control';
import { SecurityCheckupService } from './security_checkup';
import { SessionExpired, SessionTimeout, UnauthorizedResponseHttpInterceptor } from './session';
import { SecurityTelemetryService } from './telemetry';
import type { UiApi } from './ui_api';
import { getUiApi } from './ui_api';

Expand Down Expand Up @@ -63,6 +64,7 @@ export class SecurityPlugin
private readonly managementService = new ManagementService();
private readonly securityCheckupService: SecurityCheckupService;
private readonly anonymousAccessService = new AnonymousAccessService();
private readonly securityTelemetryService = new SecurityTelemetryService();
private authc!: AuthenticationServiceSetup;

constructor(private readonly initializerContext: PluginInitializerContext) {
Expand Down Expand Up @@ -164,6 +166,11 @@ export class SecurityPlugin
this.anonymousAccessService.start({ http: core.http });
}

this.securityTelemetryService.start({
http: core.http,
getCurrentUser: this.authc.getCurrentUser,
});

return {
uiApi: getUiApi({ core }),
navControlService: this.navControlService.start({ core }),
Expand Down
47 changes: 47 additions & 0 deletions x-pack/plugins/security/public/telemetry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { HttpStart } from 'src/core/public';

import type { AuthenticatedUser } from '../../common';

interface SecurityTelemetryStartParams {
http: HttpStart;
getCurrentUser: () => Promise<AuthenticatedUser>;
}

interface SecurityTelemetryAuthType {
username_hash: string;
timestamp: number;
auth_type: string;
}

export class SecurityTelemetryService {
public static KeyAuthType = 'kibana-user-auth-type';

public async start({ http, getCurrentUser }: SecurityTelemetryStartParams) {
// wait for the user to be authenticated before doing telemetry
getCurrentUser().then(() => this.postAuthTypeTelemetry(http));
XavierM marked this conversation as resolved.
Show resolved Hide resolved
}

private async postAuthTypeTelemetry(http: HttpStart) {
try {
const telemetryAuthTypeStringify = localStorage.getItem(SecurityTelemetryService.KeyAuthType);
const telemetryAuthTypeObj = await http.post<SecurityTelemetryAuthType>(
'/internal/security/telemetry/auth_type',
{
body: telemetryAuthTypeStringify,
}
);
localStorage.setItem(
SecurityTelemetryService.KeyAuthType,
JSON.stringify(telemetryAuthTypeObj)
);
// eslint-disable-next-line no-empty
} catch (exp) {}
XavierM marked this conversation as resolved.
Show resolved Hide resolved
}
}
4 changes: 4 additions & 0 deletions x-pack/plugins/security/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ export class SecurityPlugin

this.registerDeprecations(core, license);

// Usage counter for telemetry
const usageCounter = usageCollection?.createUsageCounter('security');

defineRoutes({
router: core.http.createRouter(),
basePath: core.http.basePath,
Expand All @@ -307,6 +310,7 @@ export class SecurityPlugin
getFeatureUsageService: this.getFeatureUsageService,
getAuthenticationService: this.getAuthentication,
getAnonymousAccessService: this.getAnonymousAccess,
usageCounter,
XavierM marked this conversation as resolved.
Show resolved Hide resolved
});

return Object.freeze<SecurityPluginSetup>({
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/security/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Observable } from 'rxjs';

import type { PublicMethodsOf } from '@kbn/utility-types';
import type { HttpResources, IBasePath, Logger } from 'src/core/server';
import type { UsageCounter } from 'src/plugins/usage_collection/server';

import type { KibanaFeature } from '../../../features/server';
import type { SecurityLicense } from '../../common';
Expand All @@ -28,6 +29,7 @@ import { defineIndicesRoutes } from './indices';
import { defineRoleMappingRoutes } from './role_mapping';
import { defineSecurityCheckupGetStateRoutes } from './security_checkup';
import { defineSessionManagementRoutes } from './session_management';
import { defineTelemetryRoutes } from './telemetry';
import { defineUsersRoutes } from './users';
import { defineViewRoutes } from './views';

Expand All @@ -48,6 +50,7 @@ export interface RouteDefinitionParams {
getFeatureUsageService: () => SecurityFeatureUsageServiceStart;
getAuthenticationService: () => InternalAuthenticationServiceStart;
getAnonymousAccessService: () => AnonymousAccessServiceStart;
usageCounter?: UsageCounter;
}

export function defineRoutes(params: RouteDefinitionParams) {
Expand All @@ -62,4 +65,5 @@ export function defineRoutes(params: RouteDefinitionParams) {
defineDeprecationsRoutes(params);
defineAnonymousAccessRoutes(params);
defineSecurityCheckupGetStateRoutes(params);
defineTelemetryRoutes(params);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import type { RequestHandler } from 'src/core/server';
import { kibanaResponseFactory } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock';

import type { UsageCounter } from '../../../../../../src/plugins/usage_collection/server/usage_counters/usage_counter';
import type { AuthenticatedUser } from '../../../common';
import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock';
import type { InternalAuthenticationServiceStart } from '../../authentication';
import { authenticationServiceMock } from '../../authentication/authentication_service.mock';
import type { SecurityRequestHandlerContext } from '../../types';
import { routeDefinitionParamsMock } from '../index.mock';
import { defineTelemetryOnAuthTypeRoutes } from './authentication_type';

const FAKE_TIMESTAMP = 1637665318135;
function getMockContext(
licenseCheckResult: { state: string; message?: string } = { state: 'valid' }
) {
return {
licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } },
} as unknown as SecurityRequestHandlerContext;
}

describe('Telemetry on auth type', () => {
const mockContext = getMockContext();
let routeHandler: RequestHandler<any, any, any, any>;
let authc: DeeplyMockedKeys<InternalAuthenticationServiceStart>;

jest.useFakeTimers('modern').setSystemTime(FAKE_TIMESTAMP);

describe('call incrementCounter', () => {
let mockUsageCounter: UsageCounter;
beforeEach(() => {
const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
authc = authenticationServiceMock.createStart();
authc.getCurrentUser.mockImplementation(() => mockAuthenticatedUser());
const mockRouteDefinitionParams = {
...routeDefinitionParamsMock.create(),
usageCounter: mockUsageCounter,
};
mockRouteDefinitionParams.getAuthenticationService.mockReturnValue(authc);
defineTelemetryOnAuthTypeRoutes(mockRouteDefinitionParams);

const [, telemetryOnAuthTypeRouteHandler] =
mockRouteDefinitionParams.router.post.mock.calls.find(
([{ path }]) => path === '/internal/security/telemetry/auth_type'
)!;
routeHandler = telemetryOnAuthTypeRouteHandler;
});

it('if request body is equal to null.', async () => {
const request = httpServerMock.createKibanaRequest();
const response = await routeHandler(mockContext, request, kibanaResponseFactory);

expect(response.status).toBe(200);
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1);
expect(response.payload).toEqual({
auth_type: 'realm',
timestamp: FAKE_TIMESTAMP,
username_hash: '8ac76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
});
});

it('if elapsed time is above 12 hours', async () => {
const request = httpServerMock.createKibanaRequest({
body: {
auth_type: 'realm',
timestamp: FAKE_TIMESTAMP - 13 * 60 * 60 * 1000,
username_hash: '8ac76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
},
});
const response = await routeHandler(mockContext, request, kibanaResponseFactory);

expect(response.status).toBe(200);
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1);
expect(response.payload).toEqual({
auth_type: 'realm',
timestamp: FAKE_TIMESTAMP,
username_hash: '8ac76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
});
});

it('if authType changed', async () => {
const request = httpServerMock.createKibanaRequest({
body: {
auth_type: 'token',
timestamp: FAKE_TIMESTAMP,
username_hash: '8ac76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
},
});
const response = await routeHandler(mockContext, request, kibanaResponseFactory);

expect(response.status).toBe(200);
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1);
expect(response.payload).toEqual({
auth_type: 'realm',
timestamp: FAKE_TIMESTAMP,
username_hash: '8ac76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
});
});

it('if username changed', async () => {
const request = httpServerMock.createKibanaRequest({
body: {
auth_type: 'token',
timestamp: FAKE_TIMESTAMP,
username_hash: '33c76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
},
});
const response = await routeHandler(mockContext, request, kibanaResponseFactory);

expect(response.status).toBe(200);
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1);
expect(response.payload).toEqual({
auth_type: 'realm',
timestamp: FAKE_TIMESTAMP,
username_hash: '8ac76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
});
});
});

describe('do NOT call incrementCounter', () => {
let mockUsageCounter: UsageCounter;
beforeEach(() => {
const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
authc = authenticationServiceMock.createStart();
authc.getCurrentUser.mockImplementation(() => mockAuthenticatedUser());
const mockRouteDefinitionParams = {
...routeDefinitionParamsMock.create(),
usageCounter: mockUsageCounter,
};
mockRouteDefinitionParams.getAuthenticationService.mockReturnValue(authc);
defineTelemetryOnAuthTypeRoutes(mockRouteDefinitionParams);

const [, telemetryOnAuthTypeRouteHandler] =
mockRouteDefinitionParams.router.post.mock.calls.find(
([{ path }]) => path === '/internal/security/telemetry/auth_type'
)!;
routeHandler = telemetryOnAuthTypeRouteHandler;
});

it('when getAuthenticationService do not return auth type', async () => {
authc.getCurrentUser.mockImplementation(
() =>
({
...mockAuthenticatedUser(),
authentication_type: undefined,
} as unknown as AuthenticatedUser)
);
const mockRouteDefinitionParams = {
...routeDefinitionParamsMock.create(),
usageCounter: mockUsageCounter,
};
mockRouteDefinitionParams.getAuthenticationService.mockReturnValue(authc);
defineTelemetryOnAuthTypeRoutes(mockRouteDefinitionParams);

const [, telemetryOnAuthTypeRouteHandler] =
mockRouteDefinitionParams.router.post.mock.calls.find(
([{ path }]) => path === '/internal/security/telemetry/auth_type'
)!;
routeHandler = telemetryOnAuthTypeRouteHandler;

const request = httpServerMock.createKibanaRequest();
const response = await routeHandler(mockContext, request, kibanaResponseFactory);

expect(response.status).toBe(400);
expect(response.payload).toEqual({ message: 'Authentication type can not be empty' });
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(0);
});

it('if elapsed time is under 12 hours', async () => {
const oldTimestamp = FAKE_TIMESTAMP - 10 * 60 * 60 * 1000;
const request = httpServerMock.createKibanaRequest({
body: {
auth_type: 'realm',
timestamp: oldTimestamp,
username_hash: '8ac76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
},
});
const response = await routeHandler(mockContext, request, kibanaResponseFactory);

expect(response.status).toBe(200);
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(0);
expect(response.payload).toEqual({
auth_type: 'realm',
timestamp: oldTimestamp,
username_hash: '8ac76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
});
});

it('if authType/username did not change', async () => {
const request = httpServerMock.createKibanaRequest({
body: {
auth_type: 'realm',
timestamp: FAKE_TIMESTAMP,
username_hash: '8ac76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
},
});
const response = await routeHandler(mockContext, request, kibanaResponseFactory);

expect(response.status).toBe(200);
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(0);
expect(response.payload).toEqual({
auth_type: 'realm',
timestamp: FAKE_TIMESTAMP,
username_hash: '8ac76453d769d4fd14b3f41ad4933f9bd64321972cd002de9b847e117435b08b',
});
});
});
});
Loading