[Alerts][License] Add license checks to alerts HTTP APIs and execution#85223
Conversation
|
Pinging @elastic/kibana-alerting-services (Team:Alerting Services) |
| }, | ||
| }); | ||
| // No need to notify usage on basic alert types | ||
| if (alertType.minimumLicenseRequired !== 'basic') { |
There was a problem hiding this comment.
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.
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.
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.
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.
Renamed to getLicenseCheckForAlertType
gmmorris
left a comment
There was a problem hiding this comment.
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.
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.
The styleguide recommends returning early from functions.
There was a problem hiding this comment.
According to the styleguide will keep it as it is.
| } | ||
| } | ||
|
|
||
| public ensureLicenseForAlertType(alertType: AlertType) { |
There was a problem hiding this comment.
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.
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.
Ops, copy paste issue :-)
mikecote
left a comment
There was a problem hiding this comment.
Looking good 👍 there's a few changes I'd like to re-review once complete but this is looking great!
| producer: 'test', | ||
| }, | ||
| enabledInLicense: true, | ||
| } as RegistryAlertTypeWithAuth, |
There was a problem hiding this comment.
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.
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.
But alertType is used in the code below in the create method..
There was a problem hiding this comment.
Oh, I see 🙈 all good then.
| alertSavedObject = await this.unsecuredSavedObjectsClient.get<RawAlert>('alert', id); | ||
| } | ||
|
|
||
| this.alertTypeRegistry.ensureAlertTypeEnabled(alertSavedObject.attributes.alertTypeId); |
There was a problem hiding this comment.
I think all checks in this file that happen before authorization checks / audit logging should be moved to happen after.
| { id, data }: UpdateOptions, | ||
| { attributes, version }: SavedObject<RawAlert> | ||
| ): Promise<PartialAlert> { | ||
| this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); |
There was a problem hiding this comment.
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.
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.
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' |
gmmorris
left a comment
There was a problem hiding this comment.
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.
It seems like a duplication of sources and similar to LICENSE_TYPE in the licensing plugin?
|
|
||
| import Boom from '@hapi/boom'; | ||
| import { i18n } from '@kbn/i18n'; | ||
| import { LicenseTypeValues } from '../../alerts/common'; |
There was a problem hiding this comment.
To keep the plugins decoupled, this shouldn't be requiring code from alerts.
| 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.
To keep the plugins decoupled, this shouldn't be requiring code from alerts.
| 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.
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 <42973632+kibanamachine@users.noreply.github.com>

Resolve #85102