-
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
Extract License service from CCR and Watcher into license_api_guard plugin in x-pack #95973
Changes from 6 commits
a66cff2
394ff8a
ff254aa
87c0a21
5d1e3cf
7c34572
25bc327
f6002cc
818f6da
7d8eba5
974f824
efd968e
066a69f
7448fb1
48293ff
a0fe425
5701904
0706a25
3add225
d54f85c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/* | ||
* 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 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
export { License } from './license'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/* | ||
* 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 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { License } from './license'; | ||
|
||
describe('license_pre_routing_factory', () => { | ||
describe('#reportingFeaturePreRoutingFactory', () => { | ||
const pluginName = 'testPlugin'; | ||
const currentLicenseType = 'currentLicenseType'; | ||
|
||
const testRoute = ({ licenseState }) => { | ||
const license = new License(); | ||
const logger = { | ||
warn: jest.fn(), | ||
}; | ||
|
||
const licensingMock = { | ||
license$: { | ||
subscribe: (callback) => | ||
callback({ | ||
type: currentLicenseType, | ||
check: () => ({ state: licenseState }), | ||
getFeature: () => ({}), | ||
}), | ||
}, | ||
refresh: jest.fn(), | ||
createLicensePoller: jest.fn(), | ||
featureUsage: {}, | ||
}; | ||
|
||
license.setup({ pluginName, logger }); | ||
license.start({ | ||
pluginId: 'id', | ||
minimumLicenseType: 'basic', | ||
licensing: licensingMock, | ||
}); | ||
|
||
const route = jest.fn(); | ||
const guardedRoute = license.guardApiRoute(route); | ||
const customError = jest.fn(); | ||
guardedRoute({}, {}, { customError }); | ||
|
||
return { | ||
errorResponse: | ||
customError.mock.calls.length > 0 | ||
? customError.mock.calls[customError.mock.calls.length - 1][0] | ||
: undefined, | ||
logMesssage: | ||
logger.warn.mock.calls.length > 0 | ||
? logger.warn.mock.calls[logger.warn.mock.calls.length - 1][0] | ||
: undefined, | ||
route, | ||
}; | ||
}; | ||
|
||
describe('valid license', () => { | ||
it('the original route is called and nothing is logged', () => { | ||
const { errorResponse, logMesssage, route } = testRoute({ licenseState: 'valid' }); | ||
|
||
expect(errorResponse).toBeUndefined(); | ||
expect(logMesssage).toBeUndefined(); | ||
expect(route).toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
[ | ||
{ | ||
licenseState: 'invalid', | ||
expectedMessage: `Your ${currentLicenseType} license does not support ${pluginName}. Please upgrade your license.`, | ||
}, | ||
{ | ||
licenseState: 'expired', | ||
expectedMessage: `You cannot use ${pluginName} because your ${currentLicenseType} license has expired.`, | ||
}, | ||
{ | ||
licenseState: 'unavailable', | ||
expectedMessage: `You cannot use ${pluginName} because license information is not available at this time.`, | ||
}, | ||
].forEach(({ licenseState, expectedMessage }) => { | ||
describe(`${licenseState} license`, () => { | ||
it('replies with 403 and message and logs the message', () => { | ||
const { errorResponse, logMesssage, route } = testRoute({ licenseState }); | ||
|
||
expect(errorResponse).toEqual({ | ||
body: { | ||
message: expectedMessage, | ||
}, | ||
statusCode: 403, | ||
}); | ||
|
||
expect(logMesssage).toBe(expectedMessage); | ||
expect(route).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/* | ||
* 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 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { i18n } from '@kbn/i18n'; | ||
import { | ||
Logger, | ||
KibanaRequest, | ||
KibanaResponseFactory, | ||
RequestHandler, | ||
RequestHandlerContext, | ||
} from 'src/core/server'; | ||
|
||
/* eslint-disable @kbn/eslint/no-restricted-paths */ | ||
import type { | ||
LicenseType, | ||
LicenseCheckState, | ||
} from '../../../../../x-pack/plugins/licensing/common/types'; | ||
import type { LicensingPluginStart } from '../../../../../x-pack/plugins/licensing/server/types'; | ||
/* eslint-enable @kbn/eslint/no-restricted-paths */ | ||
|
||
interface SetupSettings { | ||
pluginName: string; | ||
logger: Logger; | ||
} | ||
|
||
interface StartSettings { | ||
pluginId: string; | ||
minimumLicenseType: LicenseType; | ||
licensing: LicensingPluginStart; | ||
} | ||
|
||
export class License { | ||
private pluginName?: string; | ||
private logger?: Logger; | ||
private licenseCheckState: LicenseCheckState = 'unavailable'; | ||
private licenseType?: LicenseType; | ||
|
||
private _isEsSecurityEnabled: boolean = false; | ||
|
||
setup({ pluginName, logger }: SetupSettings) { | ||
this.pluginName = pluginName; | ||
this.logger = logger; | ||
} | ||
|
||
start({ pluginId, minimumLicenseType, licensing }: StartSettings) { | ||
licensing.license$.subscribe((license) => { | ||
this.licenseType = license.type; | ||
this.licenseCheckState = license.check(pluginId, minimumLicenseType!).state; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To testers: you can hardcode a value here, e.g. 'invalid', to test the behavior under various conditions. |
||
|
||
// Retrieving security checks the results of GET /_xpack as well as license state, | ||
// so we're also checking whether security is disabled in elasticsearch.yml. | ||
this._isEsSecurityEnabled = license.getFeature('security').isEnabled; | ||
}); | ||
} | ||
|
||
getLicenseErrorMessage(licenseCheckState: LicenseCheckState): string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit; if this does not need to be public I would mark it as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea! |
||
switch (licenseCheckState) { | ||
case 'invalid': | ||
return i18n.translate('esUi.license.errorUnsupportedMessage', { | ||
defaultMessage: | ||
'Your {licenseType} license does not support {pluginName}. Please upgrade your license.', | ||
values: { licenseType: this.licenseType!, pluginName: this.pluginName }, | ||
}); | ||
|
||
case 'expired': | ||
return i18n.translate('esUi.license.errorExpiredMessage', { | ||
defaultMessage: | ||
'You cannot use {pluginName} because your {licenseType} license has expired.', | ||
values: { licenseType: this.licenseType!, pluginName: this.pluginName }, | ||
}); | ||
|
||
case 'unavailable': | ||
return i18n.translate('esUi.license.errorUnavailableMessage', { | ||
defaultMessage: | ||
'You cannot use {pluginName} because license information is not available at this time.', | ||
values: { pluginName: this.pluginName }, | ||
}); | ||
} | ||
|
||
return i18n.translate('esUi.license.genericErrorMessage', { | ||
defaultMessage: 'You cannot use {pluginName} because the license check failed.', | ||
values: { pluginName: this.pluginName }, | ||
}); | ||
} | ||
|
||
guardApiRoute<Context extends RequestHandlerContext, Params, Query, Body>( | ||
handler: RequestHandler<Params, Query, Body, Context> | ||
) { | ||
return ( | ||
ctx: Context, | ||
request: KibanaRequest<Params, Query, Body>, | ||
response: KibanaResponseFactory | ||
) => { | ||
// We'll only surface license errors if users attempt disallowed access to the API. | ||
if (this.licenseCheckState !== 'valid') { | ||
const licenseErrorMessage = this.getLicenseErrorMessage(this.licenseCheckState); | ||
this.logger?.warn(licenseErrorMessage); | ||
|
||
return response.customError({ | ||
body: { | ||
message: licenseErrorMessage, | ||
}, | ||
statusCode: 403, | ||
}); | ||
} | ||
|
||
return handler(ctx, request, response); | ||
}; | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility | ||
get isEsSecurityEnabled() { | ||
return this._isEsSecurityEnabled; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,5 +5,4 @@ | |
* 2.0. | ||
*/ | ||
|
||
export { License } from './license'; | ||
export { addBasePath } from './add_base_path'; |
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.
@elastic/kibana-core Do we still need this linting rule to disallow importing x-pack modules into non-x-pack?
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.
For concrete imports, we definitely do, as it would plainly breaks on OSS distrib.
For types, it's not a 'technical' requirement, but until the basic plugins are moved out of xpack, we should stick with that rule.
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.
AFAIK we don't release OSS distributable starting from v7.11 https://www.elastic.co/downloads/past-releases#kibana-oss
But I'd rather plugins didn't import from
x-pack
. Note that the code insrc/
andx-pack
have different licenses, so it's easy to leak commercial code outside by importingruntime
code accidentally.Regarding the current use case: why
licensing
code lives insrc/
at all?Licensing
compatible types can be declared in the current file, but maybe anx-pack
plugin is a better place?