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

Licensed feature usage for connectors #77679

Merged
merged 23 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
43f4b4a
Initial work
mikecote Sep 16, 2020
b852474
Merge branch 'master' of github.com:elastic/kibana into actions/featu…
mikecote Sep 17, 2020
de07fbb
Merge branch 'master' of github.com:elastic/kibana into actions/featu…
mikecote Sep 17, 2020
d26630b
Merge branch 'master' of github.com:elastic/kibana into actions/featu…
mikecote Sep 18, 2020
b34c812
Fix type check and jest failures
mikecote Sep 18, 2020
7d805b9
Add unit tests
mikecote Sep 18, 2020
1abe9e8
No need to notifyUsage from alert execution handler
mikecote Sep 18, 2020
f3cbabf
Fix ESLint
mikecote Sep 18, 2020
1a16caa
Merge branch 'master' of github.com:elastic/kibana into actions/featu…
mikecote Sep 21, 2020
54afd7a
Log action usage from alerts
mikecote Sep 22, 2020
d538a1d
Merge branch 'master' of github.com:elastic/kibana into actions/featu…
mikecote Sep 29, 2020
3a588db
Add integration tests
mikecote Sep 29, 2020
718cab3
Fix jest test
mikecote Sep 29, 2020
91287d2
Skip feature usage of basic action types
mikecote Sep 29, 2020
9d682bc
Merge branch 'master' into actions/feature-usage
elasticmachine Oct 1, 2020
b38caf7
Merge branch 'master' into actions/feature-usage
elasticmachine Oct 1, 2020
542cddd
Merge branch 'master' of github.com:elastic/kibana into actions/featu…
mikecote Oct 2, 2020
2969d16
Merge branch 'actions/feature-usage' of github.com:mikecote/kibana in…
mikecote Oct 2, 2020
c621e0f
Merge with master
mikecote Oct 14, 2020
f2e1dba
Fix types
mikecote Oct 14, 2020
90718e5
Merge branch 'master' of github.com:elastic/kibana into actions/featu…
mikecote Oct 15, 2020
a6b38d4
Fix ESLint issue
mikecote Oct 15, 2020
f9368fd
Clarify comment
mikecote Oct 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 77 additions & 3 deletions x-pack/plugins/actions/server/action_type_registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ActionExecutor, ExecutorError, ILicenseState, TaskRunnerFactory } from
import { actionsConfigMock } from './actions_config.mock';
import { licenseStateMock } from './lib/license_state.mock';
import { ActionsConfigurationUtilities } from './actions_config';
import { licensingMock } from '../../licensing/server/mocks';

const mockTaskManager = taskManagerMock.setup();
let mockedLicenseState: jest.Mocked<ILicenseState>;
Expand All @@ -22,6 +23,7 @@ beforeEach(() => {
mockedLicenseState = licenseStateMock.create();
mockedActionsConfig = actionsConfigMock.create();
actionTypeRegistryParams = {
licensing: licensingMock.createSetup(),
taskManager: mockTaskManager,
taskRunnerFactory: new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
Expand Down Expand Up @@ -51,7 +53,7 @@ describe('register()', () => {
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
minimumLicenseRequired: 'gold',
executor,
});
expect(actionTypeRegistry.has('my-action-type')).toEqual(true);
Expand All @@ -69,6 +71,10 @@ describe('register()', () => {
},
]
`);
expect(actionTypeRegistryParams.licensing.featureUsage.register).toHaveBeenCalledWith(
'Connector: My action type',
'gold'
);
});

test('shallow clones the given action type', () => {
Expand Down Expand Up @@ -123,6 +129,31 @@ describe('register()', () => {
expect(getRetry(0, new ExecutorError('my message', {}, undefined))).toEqual(false);
expect(getRetry(0, new ExecutorError('my message', {}, retryTime))).toEqual(retryTime);
});

test('registers gold+ action types to the licensing feature usage API', () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'gold',
executor,
});
expect(actionTypeRegistryParams.licensing.featureUsage.register).toHaveBeenCalledWith(
'Connector: My action type',
'gold'
);
});

test(`doesn't register basic action types to the licensing feature usage API`, () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
executor,
});
expect(actionTypeRegistryParams.licensing.featureUsage.register).not.toHaveBeenCalled();
});
});

describe('get()', () => {
Expand Down Expand Up @@ -232,10 +263,20 @@ describe('isActionTypeEnabled', () => {
expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true);
});

test('should call isLicenseValidForActionType of the license state', async () => {
test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => {
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
actionTypeRegistry.isActionTypeEnabled('foo');
expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType);
expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, {
notifyUsage: false,
});
});

test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', async () => {
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
actionTypeRegistry.isActionTypeEnabled('foo', { notifyUsage: true });
expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, {
notifyUsage: true,
});
});

test('should return false when isActionTypeEnabled is false and isLicenseValidForActionType is true', async () => {
Expand Down Expand Up @@ -298,3 +339,36 @@ describe('ensureActionTypeEnabled', () => {
).toThrowErrorMatchingInlineSnapshot(`"Fail"`);
});
});

describe('isActionExecutable()', () => {
let actionTypeRegistry: ActionTypeRegistry;
const fooActionType: ActionType = {
id: 'foo',
name: 'Foo',
minimumLicenseRequired: 'basic',
executor: async (options) => {
return { status: 'ok', actionId: options.actionId };
},
};

beforeEach(() => {
actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register(fooActionType);
});

test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => {
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
actionTypeRegistry.isActionExecutable('123', 'foo');
expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, {
notifyUsage: false,
});
});

test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', async () => {
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
actionTypeRegistry.isActionExecutable('123', 'foo', { notifyUsage: true });
expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, {
notifyUsage: true,
});
});
});
37 changes: 31 additions & 6 deletions x-pack/plugins/actions/server/action_type_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
import Boom from 'boom';
import { i18n } from '@kbn/i18n';
import { RunContext, TaskManagerSetupContract } from '../../task_manager/server';
import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib';
import { ActionType as CommonActionType } from '../common';
import { ActionsConfigurationUtilities } from './actions_config';
import { LicensingPluginSetup } from '../../licensing/server';
import {
ExecutorError,
getActionTypeFeatureUsageName,
TaskRunnerFactory,
ILicenseState,
} from './lib';
import {
ActionType,
PreConfiguredAction,
Expand All @@ -19,6 +25,7 @@ import {
} from './types';

export interface ActionTypeRegistryOpts {
licensing: LicensingPluginSetup;
taskManager: TaskManagerSetupContract;
taskRunnerFactory: TaskRunnerFactory;
actionsConfigUtils: ActionsConfigurationUtilities;
Expand All @@ -33,13 +40,15 @@ export class ActionTypeRegistry {
private readonly actionsConfigUtils: ActionsConfigurationUtilities;
private readonly licenseState: ILicenseState;
private readonly preconfiguredActions: PreConfiguredAction[];
private readonly licensing: LicensingPluginSetup;

constructor(constructorParams: ActionTypeRegistryOpts) {
this.taskManager = constructorParams.taskManager;
this.taskRunnerFactory = constructorParams.taskRunnerFactory;
this.actionsConfigUtils = constructorParams.actionsConfigUtils;
this.licenseState = constructorParams.licenseState;
this.preconfiguredActions = constructorParams.preconfiguredActions;
this.licensing = constructorParams.licensing;
}

/**
Expand All @@ -54,26 +63,35 @@ export class ActionTypeRegistry {
*/
public ensureActionTypeEnabled(id: string) {
this.actionsConfigUtils.ensureActionTypeEnabled(id);
// Important to happen last due to feature usage being notified at the same time
mikecote marked this conversation as resolved.
Show resolved Hide resolved
this.licenseState.ensureLicenseForActionType(this.get(id));
}

/**
* Returns true if action type is enabled in the config and a valid license is used.
*/
public isActionTypeEnabled(id: string) {
public isActionTypeEnabled(
id: string,
options: { notifyUsage: boolean } = { notifyUsage: false }
) {
return (
this.actionsConfigUtils.isActionTypeEnabled(id) &&
this.licenseState.isLicenseValidForActionType(this.get(id)).isValid === true
this.licenseState.isLicenseValidForActionType(this.get(id), options).isValid === true
);
}

/**
* Returns true if action type is enabled or it is a preconfigured action type.
*/
public isActionExecutable(actionId: string, actionTypeId: string) {
public isActionExecutable(
actionId: string,
actionTypeId: string,
options: { notifyUsage: boolean } = { notifyUsage: false }
) {
const actionTypeEnabled = this.isActionTypeEnabled(actionTypeId, options);
return (
this.isActionTypeEnabled(actionTypeId) ||
(!this.isActionTypeEnabled(actionTypeId) &&
actionTypeEnabled ||
(!actionTypeEnabled &&
this.preconfiguredActions.find(
(preconfiguredAction) => preconfiguredAction.id === actionId
) !== undefined)
Expand Down Expand Up @@ -118,6 +136,13 @@ export class ActionTypeRegistry {
createTaskRunner: (context: RunContext) => this.taskRunnerFactory.create(context),
},
});
// No need to notify usage on basic action types
if (actionType.minimumLicenseRequired !== 'basic') {
this.licensing.featureUsage.register(
getActionTypeFeatureUsageName(actionType as ActionType),
actionType.minimumLicenseRequired
);
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/actions/server/actions_client.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const createActionsClientMock = () => {
execute: jest.fn(),
enqueueExecution: jest.fn(),
listTypes: jest.fn(),
isActionTypeEnabled: jest.fn(),
};
return mocked;
};
Expand Down
33 changes: 32 additions & 1 deletion x-pack/plugins/actions/server/actions_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { schema } from '@kbn/config-schema';

import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry';
import { ActionsClient } from './actions_client';
import { ExecutorType } from './types';
import { ExecutorType, ActionType } from './types';
import { ActionExecutor, TaskRunnerFactory, ILicenseState } from './lib';
import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
import { actionsConfigMock } from './actions_config.mock';
import { getActionsConfigurationUtilities } from './actions_config';
import { licenseStateMock } from './lib/license_state.mock';
import { licensingMock } from '../../licensing/server/mocks';

import {
elasticsearchServiceMock,
Expand Down Expand Up @@ -47,6 +48,7 @@ beforeEach(() => {
jest.resetAllMocks();
mockedLicenseState = licenseStateMock.create();
actionTypeRegistryParams = {
licensing: licensingMock.createSetup(),
taskManager: mockTaskManager,
taskRunnerFactory: new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
Expand Down Expand Up @@ -299,6 +301,7 @@ describe('create()', () => {
});

const localActionTypeRegistryParams = {
licensing: licensingMock.createSetup(),
taskManager: mockTaskManager,
taskRunnerFactory: new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
Expand Down Expand Up @@ -1244,3 +1247,31 @@ describe('enqueueExecution()', () => {
expect(executionEnqueuer).toHaveBeenCalledWith(unsecuredSavedObjectsClient, opts);
});
});

describe('isActionTypeEnabled()', () => {
const fooActionType: ActionType = {
id: 'foo',
name: 'Foo',
minimumLicenseRequired: 'gold',
executor: jest.fn(),
};
beforeEach(() => {
actionTypeRegistry.register(fooActionType);
});

test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', () => {
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
actionsClient.isActionTypeEnabled('foo');
expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, {
notifyUsage: false,
});
});

test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', () => {
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
actionsClient.isActionTypeEnabled('foo', { notifyUsage: true });
expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, {
notifyUsage: true,
});
});
});
7 changes: 7 additions & 0 deletions x-pack/plugins/actions/server/actions_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,13 @@ export class ActionsClient {
public async listTypes(): Promise<ActionType[]> {
return this.actionTypeRegistry.list();
}

public isActionTypeEnabled(
actionTypeId: string,
options: { notifyUsage: boolean } = { notifyUsage: false }
) {
return this.actionTypeRegistry.isActionTypeEnabled(actionTypeId, options);
}
}

function actionFromSavedObject(savedObject: SavedObject<RawAction>): ActionResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Logger } from '../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
import { actionsConfigMock } from '../actions_config.mock';
import { licenseStateMock } from '../lib/license_state.mock';
import { licensingMock } from '../../../licensing/server/mocks';

const ACTION_TYPE_IDS = ['.index', '.email', '.pagerduty', '.server-log', '.slack', '.webhook'];

Expand All @@ -21,6 +22,7 @@ export function createActionTypeRegistry(): {
} {
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
const actionTypeRegistry = new ActionTypeRegistry({
licensing: licensingMock.createSetup(),
taskManager: taskManagerMock.setup(),
taskRunnerFactory: new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ beforeEach(() => jest.resetAllMocks());

describe('execute()', () => {
test('schedules the action with all given parameters', async () => {
const actionTypeRegistry = actionTypeRegistryMock.create();
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
actionTypeRegistry: actionTypeRegistryMock.create(),
actionTypeRegistry,
isESOUsingEphemeralEncryptionKey: false,
preconfiguredActions: [],
});
Expand Down Expand Up @@ -76,6 +77,9 @@ describe('execute()', () => {
},
{}
);
expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('123', 'mock-action', {
notifyUsage: true,
});
});

test('schedules the action with all given parameters with a preconfigured action', async () => {
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/actions/server/create_execute_function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function createExecutionEnqueuerFunction({
id
);

if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) {
if (!actionTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) {
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
}

Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/actions/server/lib/action_executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ test('successfully executes', async () => {
);

expect(actionTypeRegistry.get).toHaveBeenCalledWith('test');
expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('1', 'test', {
notifyUsage: true,
});

expect(actionType.executor).toHaveBeenCalledWith({
actionId: '1',
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/actions/server/lib/action_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class ActionExecutor {
namespace.namespace
);

if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId)) {
if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId, { notifyUsage: true })) {
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
}
const actionType = actionTypeRegistry.get(actionTypeId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { ActionType } from '../types';

export function getActionTypeFeatureUsageName(actionType: ActionType) {
return `Connector: ${actionType.name}`;
}
1 change: 1 addition & 0 deletions x-pack/plugins/actions/server/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export { TaskRunnerFactory } from './task_runner_factory';
export { ActionExecutor, ActionExecutorContract } from './action_executor';
export { ILicenseState, LicenseState } from './license_state';
export { verifyApiAccess } from './verify_api_access';
export { getActionTypeFeatureUsageName } from './get_action_type_feature_usage_name';
export {
ActionTypeDisabledError,
ActionTypeDisabledReason,
Expand Down
Loading