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

[Alerts][License] Add license checks to alerts HTTP APIs and execution #85223

Conversation

YulNaumenko
Copy link
Contributor

@YulNaumenko YulNaumenko commented Dec 8, 2020

Resolve #85102

  1. Was added 403 Forbidder response for executing alert API (create, update, enable, disable, muteAll, unmuteAll, muteInstance, unmuteInstance) with the higher level alert type license.
  2. Delete API remain available to remove higher license alerts.
  3. When the task is running for a scheduled alert with the higher level license, the event log will provide the next execution status:
executionStatus: {lastExecutionDate: "2020-12-09T06:30:15.749Z", status: "error", error: {reason: "license",…}}
error: {reason: "license",…}
message: "Alert type .geo-threshold is disabled because your basic license does not support it. Please upgrade your license."
reason: "license"
lastExecutionDate: "2020-12-09T06:30:15.749Z"
status: "error"

@YulNaumenko YulNaumenko added Feature:Alerting v8.0.0 release_note:skip Skip the PR/issue when compiling release notes Team:ResponseOps Label for the ResponseOps team (formerly the Cases and Alerting teams) v7.11.0 labels Dec 8, 2020
@YulNaumenko YulNaumenko self-assigned this Dec 8, 2020
@YulNaumenko YulNaumenko marked this pull request as ready for review December 8, 2020 20:55
@YulNaumenko YulNaumenko requested a review from a team as a code owner December 8, 2020 20:55
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-alerting-services (Team:Alerting Services)

@@ -108,6 +123,13 @@ export class AlertTypeRegistry {
this.taskRunnerFactory.create(normalizedAlertType, context),
},
});
// No need to notify usage on basic alert types
if (alertType.minimumLicenseRequired !== 'basic') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit.
Is there an enum we can use to reference the basic string?
I'm always worried when we hard code values like this.

Comment on lines 183 to 184
this.licenseState.isLicenseValidForAlertType(id, name, minimumLicenseRequired)
.isValid === true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit.
Since this is a boolean, the === seems redundant. :)

Suggested change
this.licenseState.isLicenseValidForAlertType(id, name, minimumLicenseRequired)
.isValid === true,
this.licenseState.isLicenseValidForAlertType(id, name, minimumLicenseRequired)
.isValid,

this._notifyUsage = notifyUsage;
}

public isLicenseValidForAlertType(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit.
Generally speaking I'd expecta method that starts with is... to return a boolean, while here we return a complex type.
It's no biggie, but I know this is a common pattern so I thought it's worth noting.

Comment on lines +60 to +62
if (notifyUsage) {
this.notifyUsage(alertTypeName, minimumLicenseRequired);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely wouldn't expect a method that starts with is... to have a side effect like this.
Should the notification be extracted from here and have it's own entry point?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to getLicenseCheckForAlertType

Copy link
Contributor

@gmmorris gmmorris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still need to do some manual testing, but wanted to post my notes so far so you can start looking into them.

Some concerns around missing tests, and code flow, but nothing major so far :)

Comment on lines +142 to +178
if (check.isValid) {
return;
}
switch (check.reason) {
case 'unavailable':
throw new AlertTypeDisabledError(
i18n.translate('xpack.alerts.serverSideErrors.unavailableLicenseErrorMessage', {
defaultMessage:
'Alert type {alertTypeId} is disabled because license information is not available at this time.',
values: {
alertTypeId: alertType.id,
},
}),
'license_unavailable'
);
case 'expired':
throw new AlertTypeDisabledError(
i18n.translate('xpack.alerts.serverSideErrors.expirerdLicenseErrorMessage', {
defaultMessage:
'Alert type {alertTypeId} is disabled because your {licenseType} license has expired.',
values: { alertTypeId: alertType.id, licenseType: this.license!.type },
}),
'license_expired'
);
case 'invalid':
throw new AlertTypeDisabledError(
i18n.translate('xpack.alerts.serverSideErrors.invalidLicenseErrorMessage', {
defaultMessage:
'Alert type {alertTypeId} is disabled because your {licenseType} license does not support it. Please upgrade your license.',
values: { alertTypeId: alertType.id, licenseType: this.license!.type },
}),
'license_invalid'
);
default:
assertNever(check.reason);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit.
early returns can be error prone because developers often don't expect them.
It means there's an implicit assumption that if isValid is false, then the switch will not be called- this can easily be missed with an early return, so I've moved it into the if.

Suggested change
if (check.isValid) {
return;
}
switch (check.reason) {
case 'unavailable':
throw new AlertTypeDisabledError(
i18n.translate('xpack.alerts.serverSideErrors.unavailableLicenseErrorMessage', {
defaultMessage:
'Alert type {alertTypeId} is disabled because license information is not available at this time.',
values: {
alertTypeId: alertType.id,
},
}),
'license_unavailable'
);
case 'expired':
throw new AlertTypeDisabledError(
i18n.translate('xpack.alerts.serverSideErrors.expirerdLicenseErrorMessage', {
defaultMessage:
'Alert type {alertTypeId} is disabled because your {licenseType} license has expired.',
values: { alertTypeId: alertType.id, licenseType: this.license!.type },
}),
'license_expired'
);
case 'invalid':
throw new AlertTypeDisabledError(
i18n.translate('xpack.alerts.serverSideErrors.invalidLicenseErrorMessage', {
defaultMessage:
'Alert type {alertTypeId} is disabled because your {licenseType} license does not support it. Please upgrade your license.',
values: { alertTypeId: alertType.id, licenseType: this.license!.type },
}),
'license_invalid'
);
default:
assertNever(check.reason);
}
}
if (!check.isValid) {
switch (check.reason) {
case 'unavailable':
throw new AlertTypeDisabledError(
i18n.translate('xpack.alerts.serverSideErrors.unavailableLicenseErrorMessage', {
defaultMessage:
'Alert type {alertTypeId} is disabled because license information is not available at this time.',
values: {
alertTypeId: alertType.id,
},
}),
'license_unavailable'
);
case 'expired':
throw new AlertTypeDisabledError(
i18n.translate('xpack.alerts.serverSideErrors.expirerdLicenseErrorMessage', {
defaultMessage:
'Alert type {alertTypeId} is disabled because your {licenseType} license has expired.',
values: { alertTypeId: alertType.id, licenseType: this.license!.type },
}),
'license_expired'
);
case 'invalid':
throw new AlertTypeDisabledError(
i18n.translate('xpack.alerts.serverSideErrors.invalidLicenseErrorMessage', {
defaultMessage:
'Alert type {alertTypeId} is disabled because your {licenseType} license does not support it. Please upgrade your license.',
values: { alertTypeId: alertType.id, licenseType: this.license!.type },
}),
'license_invalid'
);
default:
assertNever(check.reason);
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The styleguide recommends returning early from functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the styleguide will keep it as it is.

@@ -78,6 +129,53 @@ export class LicenseState {
return assertNever(check.state);
}
}

public ensureLicenseForAlertType(alertType: AlertType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has no unit tests - can add some?

statusCode: 403,
error: 'Forbidden',
message:
'Action type .email is disabled because your basic license does not support it. Please upgrade your license.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this test- it says it's testing a Gold AlertType, but the error is about a Gold Action type.
Feels wrong, unless I'm missing something 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ops, copy paste issue :-)

@mikecote mikecote self-requested a review December 9, 2020 12:56
Copy link
Contributor

@mikecote mikecote left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good 👍 there's a few changes I'd like to re-review once complete but this is looking great!

@@ -52,7 +53,8 @@ describe('listAlertTypesRoute', () => {
state: [],
},
producer: 'test',
},
enabledInLicense: true,
} as RegistryAlertTypeWithAuth,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this file, it's better to do things like const listTypes: RegistryAlertTypeWithAuth[] in the above instead. Otherwise it's not fully typed to RegistryAlertTypeWithAuth from what I can see. I was able to add extra properties to the object without typecheck failing.

Comment on lines +239 to 242
this.alertTypeRegistry.ensureAlertTypeEnabled(data.alertTypeId);

// Throws an error if alert type isn't registered
const alertType = this.alertTypeRegistry.get(data.alertTypeId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ensureAlertTypeEnabled calls get internally, we can remove the .get call while keeping the comment.

Suggested change
this.alertTypeRegistry.ensureAlertTypeEnabled(data.alertTypeId);
// Throws an error if alert type isn't registered
const alertType = this.alertTypeRegistry.get(data.alertTypeId);
// Throws an error if alert type isn't registered
this.alertTypeRegistry.ensureAlertTypeEnabled(data.alertTypeId);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But alertType is used in the code below in the create method..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see 🙈 all good then.

@@ -619,6 +621,8 @@ export class AlertsClient {
alertSavedObject = await this.unsecuredSavedObjectsClient.get<RawAlert>('alert', id);
}

this.alertTypeRegistry.ensureAlertTypeEnabled(alertSavedObject.attributes.alertTypeId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all checks in this file that happen before authorization checks / audit logging should be moved to happen after.

@@ -682,6 +686,8 @@ export class AlertsClient {
{ id, data }: UpdateOptions,
{ attributes, version }: SavedObject<RawAlert>
): Promise<PartialAlert> {
this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since updateAlert is only called from updateWithOCC and updateWithOCC already ensures the alert type is enabled, we should be good to remove this.

Suggested change
this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId);

Comment on lines 31 to 39
getTestAlertData({
actions: [
{
id: createdAction.id,
group: 'default',
params: {},
},
],
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would maybe remove actions from the scenario

Suggested change
getTestAlertData({
actions: [
{
id: createdAction.id,
group: 'default',
params: {},
},
],
})
getTestAlertData()

.post(`/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
.send(
getTestAlertData({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would need to overwrite the alertTypeId to the one you created (test.gold.noop). This will also give you the error message you're looking for below.

nit: I would also remove the actions from the scenario as the test.* actions are all gold+. Otherwise, it would need to be .server-log or something along those lines.

Suggested change
getTestAlertData({
getTestAlertData({
alertTypeId: 'test.gold.noop'

Copy link
Contributor

@gmmorris gmmorris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, behaves as expected.

We should evaluate the labeling though, as the error on the UI isn't very informative.

It says:

Alert type example.always-firing is disabled because your basic license does not support it. Please upgrade your license.

Which isn't enough as the user has no idea that this alert is still running and erroring for ever.
@YulNaumenko and I have discussed adding documentation that explains the situation, ramifications and perhaps remediation recommendations so users know what to do next.

This will hopefully save us from some SDHs.

@YulNaumenko
Copy link
Contributor Author

YulNaumenko commented Dec 9, 2020

@gchaps could you please help with the providing the informative error message, when the license is expired for the remaining alert.
The idea is that the user created alert which alert type requires a Gold license, for example on the stack Trial license.
But after some time the Trial license is expired and Alert Type become disabled due to the license restriction.
The alert keeps running in the background, causing noise and costing task manager cycles. In the UI and in the Event Log the user will get the proper error message: Alert type .geo-threshold is disabled because your basic license does not support it. Please upgrade your license.
The confusing part in this message is the word Disabled. Disabling Alert means it won’t automatically restart when license is upgraded, but the disabling alert type will do the opposite.

img

Comment on lines 26 to 33
export enum LicenseTypeValues {
Basic = 'basic',
Gold = 'gold',
Platinum = 'platinum',
Enterprise = 'enterprise',
Standard = 'standard',
Trial = 'trial',
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like a duplication of sources and similar to LICENSE_TYPE in the licensing plugin?

@@ -6,6 +6,7 @@

import Boom from '@hapi/boom';
import { i18n } from '@kbn/i18n';
import { LicenseTypeValues } from '../../alerts/common';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep the plugins decoupled, this shouldn't be requiring code from alerts.

@@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n';
import type { PublicMethodsOf } from '@kbn/utility-types';
import { Observable, Subscription } from 'rxjs';
import { assertNever } from '@kbn/std';
import { LicenseTypeValues } from '../../../alerts/common';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep the plugins decoupled, this shouldn't be requiring code from alerts.

@@ -27,8 +29,8 @@ describe('license_state', () => {
it('check application link should be disabled', () => {
const licensing = licensingMock.createSetup();
const licenseState = new LicenseState(licensing.license$);
const alertingLicenseInfo = licenseState.checkLicense(getRawLicense());
expect(alertingLicenseInfo.enableAppLink).to.be(false);
const actionsLicenseInfo = licenseState.checkLicense(getRawLicense());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there's a few variables in this file referencing actions terminology when it should be alerts.

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

‼️ ERROR: no builds found for mergeBase sha [09d437f]

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@YulNaumenko YulNaumenko merged commit 93614af into elastic:alerts/license-checks Dec 9, 2020
YulNaumenko added a commit that referenced this pull request Dec 15, 2020
* [Alerts][License] Define minimum license required for each alert type (#84997)

* Define minimum license required for each alert type

* fixed typechecks

* fixed tests

* fixed tests

* fixed due to comments

* fixed due to comments

* removed file

* removed casting to LicenseType

* [Alerts][License] Add license checks to alerts HTTP APIs and execution (#85223)

* [Alerts][License] Add license checks to alerts HTTP APIs and execution

* fixed typechecks

* resolved conflicts

* resolved conflicts

* added router tests

* fixed typechecks

* added license check support for alert task running

* fixed typechecks

* added integration tests

* fixed due to comments

* fixed due to comments

* fixed tests

* fixed typechecks

* [Alerting UI][License] Disable alert types in UI when the license doesn't support it. (#85496)

* [Alerting UI][License] Disable alert types in UI when the license doesn't support it.

* fixed typechecks

* added licensing for alert list and details page

* fixed multy select menu

* fixed due to comments

* fixed due to comments

* fixed due to comments

* fixed typechecks

* fixed license error message

* fixed license error message

* fixed typechecks

* fixed license error message

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:Alerting release_note:skip Skip the PR/issue when compiling release notes Team:ResponseOps Label for the ResponseOps team (formerly the Cases and Alerting teams) v7.11.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Alerts][License] Add license checks to alerts HTTP APIs and execution
5 participants