[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>
This commit is contained in:
Chris Roberson 2021-09-23 13:42:37 -04:00 committed by GitHub
parent 616593c33c
commit 93f5cac21a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 3 deletions

View file

@ -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 () => {

View file

@ -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<void>;
private readonly ephemeralExecutionEnqueuer: ExecutionEnqueuer<RunNowResult>;
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);
}

View file

@ -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();
});
});

View file

@ -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,
});
}
}

View file

@ -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<PluginSetupContract, PluginStartCon
private eventLogService?: IEventLogService;
private eventLogger?: IEventLogger;
private isESOCanEncrypt?: boolean;
private usageCounter?: UsageCounter;
private readonly telemetryLogger: Logger;
private readonly preconfiguredActions: PreConfiguredAction[];
private readonly kibanaIndexConfig: { kibana: { index: string } };
@ -264,13 +265,13 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
}
// Usage counter for telemetry
const usageCounter = plugins.usageCollection?.createUsageCounter(ACTIONS_FEATURE_ID);
this.usageCounter = plugins.usageCollection?.createUsageCounter(ACTIONS_FEATURE_ID);
// Routes
defineRoutes(
core.http.createRouter<ActionsRequestHandlerContext>(),
this.licenseState,
usageCounter
this.usageCounter
);
// Cleanup failed execution task definition
@ -367,6 +368,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
preconfiguredActions,
}),
auditLogger: this.security?.audit.asScoped(request),
usageCounter: this.usageCounter,
});
};
@ -497,6 +499,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
actionExecutor,
instantiateAuthorization,
security,
usageCounter,
} = this;
return async function actionsRouteHandlerContext(context, request) {
@ -533,6 +536,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
preconfiguredActions,
}),
auditLogger: security?.audit.asScoped(request),
usageCounter,
});
},
listTypes: actionTypeRegistry!.list.bind(actionTypeRegistry!),