From c10cc89fff7689339a322156f69dcc45449c8f91 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 2 Dec 2025 15:01:07 +0100 Subject: [PATCH 1/3] added ref to current license in licensing plugin class --- .../plugins/shared/licensing/server/plugin.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/x-pack/platform/plugins/shared/licensing/server/plugin.ts b/x-pack/platform/plugins/shared/licensing/server/plugin.ts index 6512674d3ad65..bba05e8cf484f 100644 --- a/x-pack/platform/plugins/shared/licensing/server/plugin.ts +++ b/x-pack/platform/plugins/shared/licensing/server/plugin.ts @@ -49,7 +49,8 @@ export class LicensingPlugin implements Plugin(1); private readonly logger: Logger; private readonly config: LicenseConfigType; - private loggingSubscription?: Subscription; + private currentLicense: undefined | Readonly; + private licenseSubscription?: Subscription; private featureUsage = new FeatureUsageService(); private refresh?: () => Promise; @@ -89,7 +90,7 @@ export class LicensingPlugin implements Plugin this.currentLicense)); this.refresh = refresh; this.license$ = license$; @@ -132,7 +133,8 @@ export class LicensingPlugin implements Plugin + this.licenseSubscription = license$.subscribe((license) => { + this.currentLicense = license; this.logger.debug( () => 'Imported license information from Elasticsearch:' + @@ -141,8 +143,8 @@ export class LicensingPlugin implements Plugin { @@ -170,9 +172,9 @@ export class LicensingPlugin implements Plugin Date: Tue, 2 Dec 2025 15:02:54 +0100 Subject: [PATCH 2/3] update test --- .../server/on_pre_response_handler.test.ts | 69 ++++++++++++++----- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/x-pack/platform/plugins/shared/licensing/server/on_pre_response_handler.test.ts b/x-pack/platform/plugins/shared/licensing/server/on_pre_response_handler.test.ts index d5a3de7ee8770..3d8666a1c29d0 100644 --- a/x-pack/platform/plugins/shared/licensing/server/on_pre_response_handler.test.ts +++ b/x-pack/platform/plugins/shared/licensing/server/on_pre_response_handler.test.ts @@ -4,22 +4,47 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { BehaviorSubject } from 'rxjs'; import { createOnPreResponseHandler } from './on_pre_response_handler'; import { httpServiceMock, httpServerMock } from '@kbn/core/server/mocks'; import { licenseMock } from '../common/licensing.mock'; describe('createOnPreResponseHandler', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('sets unknown if license.signature is unset', async () => { + const refresh = jest.fn(); + const getLicense = jest.fn(() => undefined); + const toolkit = httpServiceMock.createOnPreResponseToolkit(); + + const interceptor = createOnPreResponseHandler(refresh, getLicense); + await interceptor(httpServerMock.createKibanaRequest(), { statusCode: 200 }, toolkit); + + expect(refresh).toHaveBeenCalledTimes(0); + expect(getLicense).toHaveBeenCalledTimes(1); + expect(toolkit.next).toHaveBeenCalledTimes(1); + expect(toolkit.next).toHaveBeenCalledWith({ + headers: { + 'kbn-license-sig': 'unknown', + }, + }); + }); + it('sets license.signature header immediately for non-error responses', async () => { const refresh = jest.fn(); - const license$ = new BehaviorSubject(licenseMock.createLicense({ signature: 'foo' })); + const getLicense = jest.fn(() => licenseMock.createLicense({ signature: 'foo' })); const toolkit = httpServiceMock.createOnPreResponseToolkit(); - const interceptor = createOnPreResponseHandler(refresh, license$); + const interceptor = createOnPreResponseHandler(refresh, getLicense); await interceptor(httpServerMock.createKibanaRequest(), { statusCode: 200 }, toolkit); expect(refresh).toHaveBeenCalledTimes(0); + expect(getLicense).toHaveBeenCalledTimes(1); expect(toolkit.next).toHaveBeenCalledTimes(1); expect(toolkit.next).toHaveBeenCalledWith({ headers: { @@ -29,13 +54,14 @@ describe('createOnPreResponseHandler', () => { }); it('sets license.signature header immediately for 429 error responses', async () => { const refresh = jest.fn(); - const license$ = new BehaviorSubject(licenseMock.createLicense({ signature: 'foo' })); + const getLicense = jest.fn(() => licenseMock.createLicense({ signature: 'foo' })); const toolkit = httpServiceMock.createOnPreResponseToolkit(); - const interceptor = createOnPreResponseHandler(refresh, license$); + const interceptor = createOnPreResponseHandler(refresh, getLicense); await interceptor(httpServerMock.createKibanaRequest(), { statusCode: 429 }, toolkit); expect(refresh).toHaveBeenCalledTimes(0); + expect(getLicense).toHaveBeenCalledTimes(1); expect(toolkit.next).toHaveBeenCalledTimes(1); expect(toolkit.next).toHaveBeenCalledWith({ headers: { @@ -45,24 +71,33 @@ describe('createOnPreResponseHandler', () => { }); it('sets license.signature header after refresh for other error responses', async () => { const updatedLicense = licenseMock.createLicense({ signature: 'bar' }); - const license$ = new BehaviorSubject(licenseMock.createLicense({ signature: 'foo' })); - const refresh = jest.fn().mockImplementation( - () => - new Promise((resolve) => { - setTimeout(() => { - license$.next(updatedLicense); - resolve(); - }, 50); - }) - ); + const getLicense = jest.fn(() => licenseMock.createLicense({ signature: 'foo' })); + const refresh = jest.fn().mockImplementation(async () => { + setTimeout(() => { + getLicense.mockReturnValue(updatedLicense); + }, 1); + }); const toolkit = httpServiceMock.createOnPreResponseToolkit(); - const interceptor = createOnPreResponseHandler(refresh, license$); + const interceptor = createOnPreResponseHandler(refresh, getLicense); await interceptor(httpServerMock.createKibanaRequest(), { statusCode: 400 }, toolkit); expect(refresh).toHaveBeenCalledTimes(1); + expect(getLicense).toHaveBeenCalledTimes(1); expect(toolkit.next).toHaveBeenCalledTimes(1); + expect(toolkit.next).toHaveBeenCalledWith({ + headers: { + 'kbn-license-sig': 'foo', + }, + }); + + jest.advanceTimersByTime(10); + await interceptor(httpServerMock.createKibanaRequest(), { statusCode: 400 }, toolkit); + + expect(refresh).toHaveBeenCalledTimes(2); + expect(getLicense).toHaveBeenCalledTimes(2); + expect(toolkit.next).toHaveBeenCalledTimes(2); expect(toolkit.next).toHaveBeenCalledWith({ headers: { 'kbn-license-sig': 'bar', From e8a642714c3994ae2ae75e990cf606babcc372f7 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 2 Dec 2025 15:03:21 +0100 Subject: [PATCH 3/3] do not block Kibana responses on retrieving license info from Elasticsearch --- .../licensing/server/on_pre_response_handler.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/x-pack/platform/plugins/shared/licensing/server/on_pre_response_handler.ts b/x-pack/platform/plugins/shared/licensing/server/on_pre_response_handler.ts index 90bec62d88742..f9ab7d8759fbb 100644 --- a/x-pack/platform/plugins/shared/licensing/server/on_pre_response_handler.ts +++ b/x-pack/platform/plugins/shared/licensing/server/on_pre_response_handler.ts @@ -5,14 +5,12 @@ * 2.0. */ -import type { Observable } from 'rxjs'; -import { firstValueFrom } from 'rxjs'; import type { OnPreResponseHandler } from '@kbn/core/server'; import type { ILicense } from '@kbn/licensing-types'; export function createOnPreResponseHandler( refresh: () => Promise, - license$: Observable + getLicense: () => Readonly | undefined ): OnPreResponseHandler { return async (req, res, t) => { // If we're returning an error response, refresh license info from @@ -22,9 +20,18 @@ export function createOnPreResponseHandler( // that back-pressure should be applied, and we don't need to refresh the license in these // situations. if (res.statusCode >= 400 && res.statusCode !== 429) { - await refresh(); + void refresh(); } - const license = await firstValueFrom(license$); + const license = getLicense(); + + if (!license) { + return t.next({ + headers: { + 'kbn-license-sig': 'unknown', + }, + }); + } + return t.next({ headers: { 'kbn-license-sig': license.signature,