From 93f5cac21a54a766cb68f553fe39e26c53a78653 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 23 Sep 2021 13:42:37 -0400 Subject: [PATCH] [Actions] Telemetry for RBAC exemptions (#112356) (#112993) * More telemetry * Fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../actions/server/actions_client.test.ts | 70 +++++++++++++++++++ .../plugins/actions/server/actions_client.ts | 12 ++++ .../lib/track_legacy_rbac_exemption.test.ts | 32 +++++++++ .../server/lib/track_legacy_rbac_exemption.ts | 18 +++++ x-pack/plugins/actions/server/plugin.ts | 10 ++- 5 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts create mode 100644 x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index a341cdf58b9e..7549d2ecaab7 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -20,6 +20,7 @@ import { licenseStateMock } from './lib/license_state.mock'; import { licensingMock } from '../../licensing/server/mocks'; import { httpServerMock } from '../../../../src/core/server/mocks'; import { auditServiceMock } from '../../security/server/audit/index.mock'; +import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; import { elasticsearchServiceMock, @@ -28,7 +29,12 @@ import { import { actionExecutorMock } from './lib/action_executor.mock'; import uuid from 'uuid'; import { ActionsAuthorization } from './authorization/actions_authorization'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './authorization/get_authorization_mode_by_source'; import { actionsAuthorizationMock } from './authorization/actions_authorization.mock'; +import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../src/core/server/elasticsearch/client/mocks'; @@ -38,6 +44,22 @@ jest.mock('../../../../src/core/server/saved_objects/service/lib/utils', () => ( }, })); +jest.mock('./lib/track_legacy_rbac_exemption', () => ({ + trackLegacyRBACExemption: jest.fn(), +})); + +jest.mock('./authorization/get_authorization_mode_by_source', () => { + return { + getAuthorizationModeBySource: jest.fn(() => { + return 1; + }), + AuthorizationMode: { + Legacy: 0, + RBAC: 1, + }, + }; +}); + const defaultKibanaIndex = '.kibana'; const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -47,6 +69,8 @@ const executionEnqueuer = jest.fn(); const ephemeralExecutionEnqueuer = jest.fn(); const request = httpServerMock.createKibanaRequest(); const auditLogger = auditServiceMock.create().asScoped(request); +const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); +const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const mockTaskManager = taskManagerMock.createSetup(); @@ -82,6 +106,7 @@ beforeEach(() => { request, authorization: authorization as unknown as ActionsAuthorization, auditLogger, + usageCounter: mockUsageCounter, }); }); @@ -1640,6 +1665,9 @@ describe('update()', () => { describe('execute()', () => { describe('authorization', () => { test('ensures user is authorised to excecute actions', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); await actionsClient.execute({ actionId: 'action-id', params: { @@ -1650,6 +1678,9 @@ describe('execute()', () => { }); test('throws when user is not authorised to create the type of action', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); authorization.ensureAuthorized.mockRejectedValue( new Error(`Unauthorized to execute all actions`) ); @@ -1665,6 +1696,21 @@ describe('execute()', () => { expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); + + test('tracks legacy RBAC', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.Legacy; + }); + + await actionsClient.execute({ + actionId: 'action-id', + params: { + name: 'my name', + }, + }); + + expect(trackLegacyRBACExemption as jest.Mock).toBeCalledWith('execute', mockUsageCounter); + }); }); test('calls the actionExecutor with the appropriate parameters', async () => { @@ -1756,6 +1802,9 @@ describe('execute()', () => { describe('enqueueExecution()', () => { describe('authorization', () => { test('ensures user is authorised to excecute actions', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); await actionsClient.enqueueExecution({ id: uuid.v4(), params: {}, @@ -1766,6 +1815,9 @@ describe('enqueueExecution()', () => { }); test('throws when user is not authorised to create the type of action', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); authorization.ensureAuthorized.mockRejectedValue( new Error(`Unauthorized to execute all actions`) ); @@ -1781,6 +1833,24 @@ describe('enqueueExecution()', () => { expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); + + test('tracks legacy RBAC', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.Legacy; + }); + + await actionsClient.enqueueExecution({ + id: uuid.v4(), + params: {}, + spaceId: 'default', + apiKey: null, + }); + + expect(trackLegacyRBACExemption as jest.Mock).toBeCalledWith( + 'enqueueExecution', + mockUsageCounter + ); + }); }); test('calls the executionEnqueuer with the appropriate parameters', async () => { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index d6f6037ecd8b..b391e50283ad 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import type { estypes } from '@elastic/elasticsearch'; +import { UsageCounter } from 'src/plugins/usage_collection/server'; import { i18n } from '@kbn/i18n'; import { omitBy, isUndefined } from 'lodash'; @@ -42,6 +43,7 @@ import { } from './authorization/get_authorization_mode_by_source'; import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events'; import { RunNowResult } from '../../task_manager/server'; +import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -74,6 +76,7 @@ interface ConstructorOptions { request: KibanaRequest; authorization: ActionsAuthorization; auditLogger?: AuditLogger; + usageCounter?: UsageCounter; } export interface UpdateOptions { @@ -93,6 +96,7 @@ export class ActionsClient { private readonly executionEnqueuer: ExecutionEnqueuer; private readonly ephemeralExecutionEnqueuer: ExecutionEnqueuer; private readonly auditLogger?: AuditLogger; + private readonly usageCounter?: UsageCounter; constructor({ actionTypeRegistry, @@ -106,6 +110,7 @@ export class ActionsClient { request, authorization, auditLogger, + usageCounter, }: ConstructorOptions) { this.actionTypeRegistry = actionTypeRegistry; this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; @@ -118,6 +123,7 @@ export class ActionsClient { this.request = request; this.authorization = authorization; this.auditLogger = auditLogger; + this.usageCounter = usageCounter; } /** @@ -478,6 +484,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('execute', this.usageCounter); } return this.actionExecutor.execute({ actionId, @@ -495,6 +503,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('enqueueExecution', this.usageCounter); } return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options); } @@ -506,6 +516,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('ephemeralEnqueuedExecution', this.usageCounter); } return this.ephemeralExecutionEnqueuer(this.unsecuredSavedObjectsClient, options); } diff --git a/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts new file mode 100644 index 000000000000..ffd8e7f17c11 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts @@ -0,0 +1,32 @@ +/* + * 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 { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; +import { trackLegacyRBACExemption } from './track_legacy_rbac_exemption'; + +describe('trackLegacyRBACExemption', () => { + it('should call `usageCounter.incrementCounter`', () => { + const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); + const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); + + trackLegacyRBACExemption('test', mockUsageCounter); + expect(mockUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `source_test`, + counterType: 'legacyRBACExemption', + incrementBy: 1, + }); + }); + + it('should do nothing if no usage counter is provided', () => { + let err; + try { + trackLegacyRBACExemption('test', undefined); + } catch (e) { + err = e; + } + expect(err).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts new file mode 100644 index 000000000000..73c859c4cd21 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts @@ -0,0 +1,18 @@ +/* + * 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 { UsageCounter } from 'src/plugins/usage_collection/server'; + +export function trackLegacyRBACExemption(source: string, usageCounter?: UsageCounter) { + if (usageCounter) { + usageCounter.incrementCounter({ + counterName: `source_${source}`, + counterType: 'legacyRBACExemption', + incrementBy: 1, + }); + } +} diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index fe133ddb6f0a..78808b669d9e 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -6,7 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { UsageCollectionSetup, UsageCounter } from 'src/plugins/usage_collection/server'; import { PluginInitializerContext, Plugin, @@ -151,6 +151,7 @@ export class ActionsPlugin implements Plugin(), this.licenseState, - usageCounter + this.usageCounter ); // Cleanup failed execution task definition @@ -367,6 +368,7 @@ export class ActionsPlugin implements Plugin