-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[Alerts][License] Add license checks to alerts HTTP APIs and execution #85223
Conversation
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') { |
There was a problem hiding this comment.
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.
this.licenseState.isLicenseValidForAlertType(id, name, minimumLicenseRequired) | ||
.isValid === true, |
There was a problem hiding this comment.
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. :)
this.licenseState.isLicenseValidForAlertType(id, name, minimumLicenseRequired) | |
.isValid === true, | |
this.licenseState.isLicenseValidForAlertType(id, name, minimumLicenseRequired) | |
.isValid, |
this._notifyUsage = notifyUsage; | ||
} | ||
|
||
public isLicenseValidForAlertType( |
There was a problem hiding this comment.
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.
if (notifyUsage) { | ||
this.notifyUsage(alertTypeName, minimumLicenseRequired); | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to getLicenseCheckForAlertType
There was a problem hiding this 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 :)
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); | ||
} | ||
} |
There was a problem hiding this comment.
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
.
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); | |
} | |
} | |
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.', |
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ops, copy paste issue :-)
There was a problem hiding this 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, |
There was a problem hiding this comment.
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.
this.alertTypeRegistry.ensureAlertTypeEnabled(data.alertTypeId); | ||
|
||
// Throws an error if alert type isn't registered | ||
const alertType = this.alertTypeRegistry.get(data.alertTypeId); |
There was a problem hiding this comment.
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.
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); |
There was a problem hiding this comment.
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..
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); |
getTestAlertData({ | ||
actions: [ | ||
{ | ||
id: createdAction.id, | ||
group: 'default', | ||
params: {}, | ||
}, | ||
], | ||
}) |
There was a problem hiding this comment.
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
getTestAlertData({ | |
actions: [ | |
{ | |
id: createdAction.id, | |
group: 'default', | |
params: {}, | |
}, | |
], | |
}) | |
getTestAlertData() |
.post(`/api/alerts/alert`) | ||
.set('kbn-xsrf', 'foo') | ||
.send( | ||
getTestAlertData({ |
There was a problem hiding this comment.
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.
getTestAlertData({ | |
getTestAlertData({ | |
alertTypeId: 'test.gold.noop' |
There was a problem hiding this 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.
@gchaps could you please help with the providing the informative error message, when the license is expired for the remaining alert. |
export enum LicenseTypeValues { | ||
Basic = 'basic', | ||
Gold = 'gold', | ||
Platinum = 'platinum', | ||
Enterprise = 'enterprise', | ||
Standard = 'standard', | ||
Trial = 'trial', | ||
} |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
💚 Build SucceededMetrics [docs]
History
To update your PR or re-run it, just comment with: |
* [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 <[email protected]>
Resolve #85102