[Http] Replace buildNr with buildSha in static asset paths (#175898)

## Summary

Follow up of [first CDN
PR](https://github.com/elastic/kibana/pull/169408). Primary focus is
replacing our build nr with SHA that allows cache busting and maintains
anti-collision properties.

## How to test

Start Kibana as usual navigating around the app with the network tab
open in your browser of choice. Keep an eye out for any asset loading
errors. It's tricky to test every possible asset since there are many
permutations, but generally navigating around Kibana should work exactly
as it did before regarding loading bundles and assets.

## Notes
* did a high-level audit of usages of `buildNum` in `packages`, `src`
and `x-pack` adding comments where appropriate.
* In non-distributable builds (like dev) static asset paths will be
prefixed with `XXXXXXXXXXXX` instead of Node's `Number.MAX_SAFE_INTEGER`
* Added some validation to ensure the CDN url is of the expected form:
nothing trailing the pathname

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### Risk Matrix

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| We break some first or third party dependencies on existing asset
routes | Med | High | Attempting to mitgate by serving static assets
from both old and new paths where paths have updated to include the
build SHA. Additioanlly: it is very bad practice to rely on the values
of the static paths, but someone might be |
| Cache-busting is more aggressive | High | Low | Unlikely to be a big
problem, but we are not scoping more static assets to a SHA and so every
new Kibana release will require clients to, for example, download fonts
again. |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2024-02-07 09:54:41 +01:00 committed by GitHub
parent 455ee0a450
commit e90c2098f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 685 additions and 243 deletions

View file

@ -30,7 +30,7 @@ const PAGE_VARS_KEYS = [
// Deployment-specific keys
'version', // x4, split to version_major, version_minor, version_patch for easier filtering
'buildNum', // May be useful for Serverless
'buildNum', // May be useful for Serverless, TODO: replace with buildHash
'cloudId',
'deploymentId',
'projectId', // projectId and deploymentId are mutually exclusive. They shouldn't be sent in the same offering.

View file

@ -13,10 +13,12 @@ import { httpServiceMock } from '@kbn/core-http-server-mocks';
import type { InternalPluginInfo, UiPlugins } from '@kbn/core-plugins-base-server-internal';
import { registerBundleRoutes } from './register_bundle_routes';
import { FileHashCache } from './file_hash_cache';
import { BasePath, StaticAssets } from '@kbn/core-http-server-internal';
const createPackageInfo = (parts: Partial<PackageInfo> = {}): PackageInfo => ({
buildNum: 42,
buildSha: 'sha',
buildSha: 'shasha',
buildShaShort: 'sha',
dist: true,
branch: 'master',
version: '8.0.0',
@ -41,9 +43,12 @@ const createUiPlugins = (...ids: string[]): UiPlugins => ({
describe('registerBundleRoutes', () => {
let router: ReturnType<typeof httpServiceMock.createRouter>;
let staticAssets: StaticAssets;
beforeEach(() => {
router = httpServiceMock.createRouter();
const basePath = httpServiceMock.createBasePath('/server-base-path') as unknown as BasePath;
staticAssets = new StaticAssets({ basePath, cdnConfig: {} as any, shaDigest: 'sha' });
});
afterEach(() => {
@ -53,7 +58,7 @@ describe('registerBundleRoutes', () => {
it('registers core and shared-dep bundles', () => {
registerBundleRoutes({
router,
serverBasePath: '/server-base-path',
staticAssets,
packageInfo: createPackageInfo(),
uiPlugins: createUiPlugins(),
});
@ -64,39 +69,39 @@ describe('registerBundleRoutes', () => {
fileHashCache: expect.any(FileHashCache),
isDist: true,
bundlesPath: 'uiSharedDepsSrcDistDir',
publicPath: '/server-base-path/42/bundles/kbn-ui-shared-deps-src/',
routePath: '/42/bundles/kbn-ui-shared-deps-src/',
publicPath: '/server-base-path/sha/bundles/kbn-ui-shared-deps-src/',
routePath: '/sha/bundles/kbn-ui-shared-deps-src/',
});
expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, {
fileHashCache: expect.any(FileHashCache),
isDist: true,
bundlesPath: 'uiSharedDepsNpmDistDir',
publicPath: '/server-base-path/42/bundles/kbn-ui-shared-deps-npm/',
routePath: '/42/bundles/kbn-ui-shared-deps-npm/',
publicPath: '/server-base-path/sha/bundles/kbn-ui-shared-deps-npm/',
routePath: '/sha/bundles/kbn-ui-shared-deps-npm/',
});
expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, {
fileHashCache: expect.any(FileHashCache),
isDist: true,
bundlesPath: expect.stringMatching(/\/@kbn\/core\/target\/public$/),
publicPath: '/server-base-path/42/bundles/core/',
routePath: '/42/bundles/core/',
publicPath: '/server-base-path/sha/bundles/core/',
routePath: '/sha/bundles/core/',
});
expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, {
fileHashCache: expect.any(FileHashCache),
isDist: true,
bundlesPath: 'kbnMonacoBundleDir',
publicPath: '/server-base-path/42/bundles/kbn-monaco/',
routePath: '/42/bundles/kbn-monaco/',
publicPath: '/server-base-path/sha/bundles/kbn-monaco/',
routePath: '/sha/bundles/kbn-monaco/',
});
});
it('registers plugin bundles', () => {
registerBundleRoutes({
router,
serverBasePath: '/server-base-path',
staticAssets,
packageInfo: createPackageInfo(),
uiPlugins: createUiPlugins('plugin-a', 'plugin-b'),
});
@ -107,16 +112,16 @@ describe('registerBundleRoutes', () => {
fileHashCache: expect.any(FileHashCache),
isDist: true,
bundlesPath: '/plugins/plugin-a/public-target-dir',
publicPath: '/server-base-path/42/bundles/plugin/plugin-a/8.0.0/',
routePath: '/42/bundles/plugin/plugin-a/8.0.0/',
publicPath: '/server-base-path/sha/bundles/plugin/plugin-a/8.0.0/',
routePath: '/sha/bundles/plugin/plugin-a/8.0.0/',
});
expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, {
fileHashCache: expect.any(FileHashCache),
isDist: true,
bundlesPath: '/plugins/plugin-b/public-target-dir',
publicPath: '/server-base-path/42/bundles/plugin/plugin-b/8.0.0/',
routePath: '/42/bundles/plugin/plugin-b/8.0.0/',
publicPath: '/server-base-path/sha/bundles/plugin/plugin-b/8.0.0/',
routePath: '/sha/bundles/plugin/plugin-b/8.0.0/',
});
});
});

View file

@ -13,6 +13,7 @@ import { distDir as UiSharedDepsSrcDistDir } from '@kbn/ui-shared-deps-src';
import * as KbnMonaco from '@kbn/monaco/server';
import type { IRouter } from '@kbn/core-http-server';
import type { UiPlugins } from '@kbn/core-plugins-base-server-internal';
import { InternalStaticAssets } from '@kbn/core-http-server-internal';
import { FileHashCache } from './file_hash_cache';
import { registerRouteForBundle } from './bundles_route';
@ -28,56 +29,61 @@ import { registerRouteForBundle } from './bundles_route';
*/
export function registerBundleRoutes({
router,
serverBasePath,
uiPlugins,
packageInfo,
staticAssets,
}: {
router: IRouter;
serverBasePath: string;
uiPlugins: UiPlugins;
packageInfo: PackageInfo;
staticAssets: InternalStaticAssets;
}) {
const { dist: isDist, buildNum } = packageInfo;
const { dist: isDist } = packageInfo;
// rather than calculate the fileHash on every request, we
// provide a cache object to `resolveDynamicAssetResponse()` that
// will store the most recently used hashes.
const fileHashCache = new FileHashCache();
const sharedNpmDepsPath = '/bundles/kbn-ui-shared-deps-npm/';
registerRouteForBundle(router, {
publicPath: `${serverBasePath}/${buildNum}/bundles/kbn-ui-shared-deps-npm/`,
routePath: `/${buildNum}/bundles/kbn-ui-shared-deps-npm/`,
publicPath: staticAssets.prependPublicUrl(sharedNpmDepsPath) + '/',
routePath: staticAssets.prependServerPath(sharedNpmDepsPath) + '/',
bundlesPath: UiSharedDepsNpm.distDir,
fileHashCache,
isDist,
});
const sharedDepsPath = '/bundles/kbn-ui-shared-deps-src/';
registerRouteForBundle(router, {
publicPath: `${serverBasePath}/${buildNum}/bundles/kbn-ui-shared-deps-src/`,
routePath: `/${buildNum}/bundles/kbn-ui-shared-deps-src/`,
publicPath: staticAssets.prependPublicUrl(sharedDepsPath) + '/',
routePath: staticAssets.prependServerPath(sharedDepsPath) + '/',
bundlesPath: UiSharedDepsSrcDistDir,
fileHashCache,
isDist,
});
const coreBundlePath = '/bundles/core/';
registerRouteForBundle(router, {
publicPath: `${serverBasePath}/${buildNum}/bundles/core/`,
routePath: `/${buildNum}/bundles/core/`,
publicPath: staticAssets.prependPublicUrl(coreBundlePath) + '/',
routePath: staticAssets.prependServerPath(coreBundlePath) + '/',
bundlesPath: isDist
? fromRoot('node_modules/@kbn/core/target/public')
: fromRoot('src/core/target/public'),
fileHashCache,
isDist,
});
const monacoEditorPath = '/bundles/kbn-monaco/';
registerRouteForBundle(router, {
publicPath: `${serverBasePath}/${buildNum}/bundles/kbn-monaco/`,
routePath: `/${buildNum}/bundles/kbn-monaco/`,
publicPath: staticAssets.prependPublicUrl(monacoEditorPath) + '/',
routePath: staticAssets.prependServerPath(monacoEditorPath) + '/',
bundlesPath: KbnMonaco.bundleDir,
fileHashCache,
isDist,
});
[...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir, version }]) => {
const pluginBundlesPath = `/bundles/plugin/${id}/${version}/`;
registerRouteForBundle(router, {
publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/${version}/`,
routePath: `/${buildNum}/bundles/plugin/${id}/${version}/`,
publicPath: staticAssets.prependPublicUrl(pluginBundlesPath) + '/',
routePath: staticAssets.prependServerPath(pluginBundlesPath) + '/',
bundlesPath: publicTargetDir,
fileHashCache,
isDist,

View file

@ -16,8 +16,8 @@ import { httpResourcesMock } from '@kbn/core-http-resources-server-mocks';
import { PluginType } from '@kbn/core-base-common';
import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import { coreInternalLifecycleMock } from '@kbn/core-lifecycle-server-mocks';
import { CoreAppsService } from './core_app';
import { of } from 'rxjs';
import { CoreAppsService } from './core_app';
const emptyPlugins = (): UiPlugins => ({
internal: new Map(),
@ -146,7 +146,7 @@ describe('CoreApp', () => {
uiPlugins: prebootUIPlugins,
router: expect.any(Object),
packageInfo: coreContext.env.packageInfo,
serverBasePath: internalCorePreboot.http.basePath.serverBasePath,
staticAssets: expect.any(Object),
});
});
@ -245,7 +245,23 @@ describe('CoreApp', () => {
uiPlugins,
router: expect.any(Object),
packageInfo: coreContext.env.packageInfo,
serverBasePath: internalCoreSetup.http.basePath.serverBasePath,
staticAssets: expect.any(Object),
});
});
it('registers SHA-scoped and non-SHA-scoped UI bundle routes', async () => {
const uiPlugins = emptyPlugins();
internalCoreSetup.http.staticAssets.prependServerPath.mockReturnValue('/some-path');
await coreApp.setup(internalCoreSetup, uiPlugins);
expect(internalCoreSetup.http.registerStaticDir).toHaveBeenCalledTimes(2);
expect(internalCoreSetup.http.registerStaticDir).toHaveBeenCalledWith(
'/some-path',
expect.any(String)
);
expect(internalCoreSetup.http.registerStaticDir).toHaveBeenCalledWith(
'/ui/{path*}',
expect.any(String)
);
});
});

View file

@ -22,6 +22,7 @@ import type {
import type { UiPlugins } from '@kbn/core-plugins-base-server-internal';
import type { HttpResources, HttpResourcesServiceToolkit } from '@kbn/core-http-resources-server';
import type { InternalCorePreboot, InternalCoreSetup } from '@kbn/core-lifecycle-server-internal';
import type { InternalStaticAssets } from '@kbn/core-http-server-internal';
import { firstValueFrom, map, type Observable } from 'rxjs';
import { CoreAppConfig, type CoreAppConfigType, CoreAppPath } from './core_app_config';
import { registerBundleRoutes } from './bundle_routes';
@ -33,6 +34,7 @@ interface CommonRoutesParams {
httpResources: HttpResources;
basePath: IBasePath;
uiPlugins: UiPlugins;
staticAssets: InternalStaticAssets;
onResourceNotFound: (
req: KibanaRequest,
res: HttpResourcesServiceToolkit & KibanaResponseFactory
@ -77,10 +79,11 @@ export class CoreAppsService {
this.registerCommonDefaultRoutes({
basePath: corePreboot.http.basePath,
httpResources: corePreboot.httpResources.createRegistrar(router),
staticAssets: corePreboot.http.staticAssets,
router,
uiPlugins,
onResourceNotFound: async (req, res) =>
// THe API consumers might call various Kibana APIs (e.g. `/api/status`) when Kibana is still at the preboot
// The API consumers might call various Kibana APIs (e.g. `/api/status`) when Kibana is still at the preboot
// stage, and the main HTTP server that registers API handlers isn't up yet. At this stage we don't know if
// the API endpoint exists or not, and hence cannot reply with `404`. We also should not reply with completely
// unexpected response (`200 text/html` for the Core app). The only suitable option is to reply with `503`
@ -125,6 +128,7 @@ export class CoreAppsService {
this.registerCommonDefaultRoutes({
basePath: coreSetup.http.basePath,
httpResources: resources,
staticAssets: coreSetup.http.staticAssets,
router,
uiPlugins,
onResourceNotFound: async (req, res) => res.notFound(),
@ -210,6 +214,7 @@ export class CoreAppsService {
private registerCommonDefaultRoutes({
router,
basePath,
staticAssets,
uiPlugins,
onResourceNotFound,
httpResources,
@ -259,17 +264,23 @@ export class CoreAppsService {
registerBundleRoutes({
router,
uiPlugins,
staticAssets,
packageInfo: this.env.packageInfo,
serverBasePath: basePath.serverBasePath,
});
}
// After the package is built and bootstrap extracts files to bazel-bin,
// assets are exposed at the root of the package and in the package's node_modules dir
private registerStaticDirs(core: InternalCoreSetup | InternalCorePreboot) {
core.http.registerStaticDir(
'/ui/{path*}',
fromRoot('node_modules/@kbn/core-apps-server-internal/assets')
);
/**
* Serve UI from sha-scoped and not-sha-scoped paths to allow time for plugin code to migrate
* Eventually we only want to serve from the sha scoped path
*/
[core.http.staticAssets.prependServerPath('/ui/{path*}'), '/ui/{path*}'].forEach((path) => {
core.http.registerStaticDir(
path,
fromRoot('node_modules/@kbn/core-apps-server-internal/assets')
);
});
}
}

View file

@ -32,6 +32,7 @@
"@kbn/core-lifecycle-server-mocks",
"@kbn/core-ui-settings-server",
"@kbn/monaco",
"@kbn/core-http-server-internal",
],
"exclude": [
"target/**/*",

View file

@ -24,6 +24,7 @@ function createCoreContext({ production = false }: { production?: boolean } = {}
branch: 'branch',
buildNum: 100,
buildSha: 'buildSha',
buildShaShort: 'buildShaShort',
dist: false,
buildDate: new Date('2023-05-15T23:12:09.000Z'),
buildFlavor: 'traditional',

View file

@ -17,6 +17,7 @@ export type {
InternalHttpServiceStart,
} from './src/types';
export { BasePath } from './src/base_path_service';
export { type InternalStaticAssets, StaticAssets } from './src/static_assets';
export { cspConfig, CspConfig, type CspConfigType } from './src/csp';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { CdnConfig } from './cdn';
import { CdnConfig } from './cdn_config';
describe('CdnConfig', () => {
it.each([

View file

@ -14,15 +14,15 @@ export interface Input {
}
export class CdnConfig {
private url: undefined | URL;
private readonly url: undefined | URL;
constructor(url?: string) {
if (url) {
this.url = new URL(url); // This will throw for invalid URLs
this.url = new URL(url); // This will throw for invalid URLs, although should be validated before reaching this point
}
}
public get host(): undefined | string {
return this.url?.host ?? undefined;
return this.url?.host;
}
public get baseHref(): undefined | string {

View file

@ -16,8 +16,8 @@ const invalidHostnames = ['asdf$%^', '0'];
let mockHostname = 'kibana-hostname';
jest.mock('os', () => {
const original = jest.requireActual('os');
jest.mock('node:os', () => {
const original = jest.requireActual('node:os');
return {
...original,
@ -530,6 +530,29 @@ describe('restrictInternalApis', () => {
});
});
describe('cdn', () => {
it('allows correct URL', () => {
expect(config.schema.validate({ cdn: { url: 'https://cdn.example.com' } })).toMatchObject({
cdn: { url: 'https://cdn.example.com' },
});
});
it.each([['foo'], ['http:./']])('throws for invalid URL %s', (url) => {
expect(() => config.schema.validate({ cdn: { url } })).toThrowErrorMatchingInlineSnapshot(
`"[cdn.url]: expected URI with scheme [http|https]."`
);
});
it.each([
['https://cdn.example.com:1234/asd?thing=1', 'URL query string not allowed'],
['https://cdn.example.com:1234/asd#cool', 'URL fragment not allowed'],
[
'https://cdn.example.com:1234/asd?thing=1#cool',
'URL fragment not allowed, but found "#cool"\nURL query string not allowed, but found "?thing=1"',
],
])('throws for disallowed values %s', (url, expecterError) => {
expect(() => config.schema.validate({ cdn: { url } })).toThrow(expecterError);
});
});
describe('HttpConfig', () => {
it('converts customResponseHeaders to strings or arrays of strings', () => {
const httpSchema = config.schema;

View file

@ -12,8 +12,8 @@ import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal';
import { uuidRegexp } from '@kbn/core-base-server-internal';
import type { ICspConfig, IExternalUrlConfig } from '@kbn/core-http-server';
import { hostname } from 'os';
import url from 'url';
import { hostname, EOL } from 'node:os';
import url, { URL } from 'node:url';
import type { Duration } from 'moment';
import type { IHttpEluMonitorConfig } from '@kbn/core-http-server/src/elu_monitor';
@ -24,7 +24,7 @@ import {
securityResponseHeadersSchema,
parseRawSecurityResponseHeadersConfig,
} from './security_response_headers_config';
import { CdnConfig } from './cdn';
import { CdnConfig } from './cdn_config';
const validBasePathRegex = /^\/.*[^\/]$/;
@ -40,6 +40,24 @@ const validHostName = () => {
return hostname().replace(/[^\x00-\x7F]/g, '');
};
/**
* We assume the URL does not contain anything after the pathname so that
* we can safely append values to the pathname at runtime.
*/
function validateCdnURL(urlString: string): undefined | string {
const cdnURL = new URL(urlString);
const errors: string[] = [];
if (cdnURL.hash.length) {
errors.push(`URL fragment not allowed, but found "${cdnURL.hash}"`);
}
if (cdnURL.search.length) {
errors.push(`URL query string not allowed, but found "${cdnURL.search}"`);
}
if (errors.length) {
return `CDN URL "${cdnURL.href}" is invalid:${EOL}${errors.join(EOL)}`;
}
}
const configSchema = schema.object(
{
name: schema.string({ defaultValue: () => validHostName() }),
@ -60,7 +78,7 @@ const configSchema = schema.object(
},
}),
cdn: schema.object({
url: schema.maybe(schema.uri({ scheme: ['http', 'https'] })),
url: schema.maybe(schema.uri({ scheme: ['http', 'https'], validate: validateCdnURL })),
}),
cors: schema.object(
{

View file

@ -30,6 +30,7 @@ import { Readable } from 'stream';
import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';
import moment from 'moment';
import { of, Observable, BehaviorSubject } from 'rxjs';
import { mockCoreContext } from '@kbn/core-base-server-mocks';
const routerOptions: RouterOptions = {
isDev: false,
@ -54,8 +55,9 @@ let config$: Observable<HttpConfig>;
let configWithSSL: HttpConfig;
let configWithSSL$: Observable<HttpConfig>;
const loggingService = loggingSystemMock.create();
const logger = loggingService.get();
const coreContext = mockCoreContext.create();
const loggingService = coreContext.logger;
const logger = coreContext.logger.get();
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
let certificate: string;
@ -99,7 +101,7 @@ beforeEach(() => {
} as HttpConfig;
configWithSSL$ = of(configWithSSL);
server = new HttpServer(loggingService, 'tests', of(config.shutdownTimeout));
server = new HttpServer(coreContext, 'tests', of(config.shutdownTimeout));
});
afterEach(async () => {

View file

@ -48,6 +48,8 @@ import { performance } from 'perf_hooks';
import { isBoom } from '@hapi/boom';
import { identity } from 'lodash';
import { IHttpEluMonitorConfig } from '@kbn/core-http-server/src/elu_monitor';
import { Env } from '@kbn/config';
import { CoreContext } from '@kbn/core-base-server-internal';
import { HttpConfig } from './http_config';
import { adoptToHapiAuthFormat } from './lifecycle/auth';
import { adoptToHapiOnPreAuth } from './lifecycle/on_pre_auth';
@ -178,15 +180,20 @@ export class HttpServer {
private stopped = false;
private readonly log: Logger;
private readonly logger: LoggerFactory;
private readonly authState: AuthStateStorage;
private readonly authRequestHeaders: AuthHeadersStorage;
private readonly authResponseHeaders: AuthHeadersStorage;
private readonly env: Env;
constructor(
private readonly logger: LoggerFactory,
private readonly coreContext: CoreContext,
private readonly name: string,
private readonly shutdownTimeout$: Observable<Duration>
) {
const { logger, env } = this.coreContext;
this.logger = logger;
this.env = env;
this.authState = new AuthStateStorage(() => this.authRegistered);
this.authRequestHeaders = new AuthHeadersStorage();
this.authResponseHeaders = new AuthHeadersStorage();
@ -269,7 +276,11 @@ export class HttpServer {
this.setupResponseLogging();
this.setupGracefulShutdownHandlers();
const staticAssets = new StaticAssets(basePathService, config.cdn);
const staticAssets = new StaticAssets({
basePath: basePathService,
cdnConfig: config.cdn,
shaDigest: this.env.packageInfo.buildShaShort,
});
return {
registerRouter: this.registerRouter.bind(this),

View file

@ -76,8 +76,8 @@ export class HttpService
configService.atPath<ExternalUrlConfigType>(externalUrlConfig.path),
]).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl)));
const shutdownTimeout$ = this.config$.pipe(map(({ shutdownTimeout }) => shutdownTimeout));
this.prebootServer = new HttpServer(logger, 'Preboot', shutdownTimeout$);
this.httpServer = new HttpServer(logger, 'Kibana', shutdownTimeout$);
this.prebootServer = new HttpServer(coreContext, 'Preboot', shutdownTimeout$);
this.httpServer = new HttpServer(coreContext, 'Kibana', shutdownTimeout$);
this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server'));
}

View file

@ -1,61 +0,0 @@
/*
* 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 { StaticAssets } from './static_assets';
import { BasePath } from './base_path_service';
import { CdnConfig } from './cdn';
describe('StaticAssets', () => {
let basePath: BasePath;
let cdnConfig: CdnConfig;
let staticAssets: StaticAssets;
beforeEach(() => {
basePath = new BasePath('/base-path');
});
describe('#getHrefBase()', () => {
it('provides fallback to server base path', () => {
cdnConfig = CdnConfig.from();
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getHrefBase()).toEqual('/base-path');
});
it('provides the correct HREF given a CDN is configured', () => {
cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' });
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getHrefBase()).toEqual('https://cdn.example.com/test');
});
});
describe('#getPluginAssetHref()', () => {
it('returns the expected value when CDN config is not set', () => {
cdnConfig = CdnConfig.from();
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getPluginAssetHref('foo', 'path/to/img.gif')).toEqual(
'/base-path/plugins/foo/assets/path/to/img.gif'
);
});
it('returns the expected value when CDN config is set', () => {
cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' });
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getPluginAssetHref('bar', 'path/to/img.gif')).toEqual(
'https://cdn.example.com/test/plugins/bar/assets/path/to/img.gif'
);
});
it('removes leading slash from the', () => {
cdnConfig = CdnConfig.from();
staticAssets = new StaticAssets(basePath, cdnConfig);
expect(staticAssets.getPluginAssetHref('dolly', '/path/for/something.svg')).toEqual(
'/base-path/plugins/dolly/assets/path/for/something.svg'
);
});
});
});

View file

@ -1,39 +0,0 @@
/*
* 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 type { BasePath } from './base_path_service';
import { CdnConfig } from './cdn';
export interface InternalStaticAssets {
getHrefBase(): string;
getPluginAssetHref(pluginName: string, assetPath: string): string;
}
export class StaticAssets implements InternalStaticAssets {
private readonly assetsHrefBase: string;
constructor(basePath: BasePath, cdnConfig: CdnConfig) {
const hrefToUse = cdnConfig.baseHref ?? basePath.serverBasePath;
this.assetsHrefBase = hrefToUse.endsWith('/') ? hrefToUse.slice(0, -1) : hrefToUse;
}
/**
* Returns a href (hypertext reference) intended to be used as the base for constructing
* other hrefs to static assets.
*/
getHrefBase(): string {
return this.assetsHrefBase;
}
getPluginAssetHref(pluginName: string, assetPath: string): string {
if (assetPath.startsWith('/')) {
assetPath = assetPath.slice(1);
}
return `${this.assetsHrefBase}/plugins/${pluginName}/assets/${assetPath}`;
}
}

View file

@ -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 { type InternalStaticAssets, StaticAssets } from './static_assets';

View file

@ -0,0 +1,132 @@
/*
* 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 { StaticAssets, type StaticAssetsParams } from './static_assets';
import { BasePath } from '../base_path_service';
import { CdnConfig } from '../cdn_config';
describe('StaticAssets', () => {
let basePath: BasePath;
let cdnConfig: CdnConfig;
let staticAssets: StaticAssets;
let args: StaticAssetsParams;
beforeEach(() => {
basePath = new BasePath('/base-path');
cdnConfig = CdnConfig.from();
args = { basePath, cdnConfig, shaDigest: '' };
});
describe('#getHrefBase()', () => {
it('provides fallback to server base path', () => {
staticAssets = new StaticAssets(args);
expect(staticAssets.getHrefBase()).toEqual('/base-path');
});
it('provides the correct HREF given a CDN is configured', () => {
args.cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' });
staticAssets = new StaticAssets(args);
expect(staticAssets.getHrefBase()).toEqual('https://cdn.example.com/test');
});
});
describe('#getPluginAssetHref()', () => {
it('returns the expected value when CDN is not configured', () => {
staticAssets = new StaticAssets(args);
expect(staticAssets.getPluginAssetHref('foo', 'path/to/img.gif')).toEqual(
'/base-path/plugins/foo/assets/path/to/img.gif'
);
});
it('returns the expected value when CDN is configured', () => {
args.cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' });
staticAssets = new StaticAssets(args);
expect(staticAssets.getPluginAssetHref('bar', 'path/to/img.gif')).toEqual(
'https://cdn.example.com/test/plugins/bar/assets/path/to/img.gif'
);
});
it('removes leading and trailing slash from the assetPath', () => {
staticAssets = new StaticAssets(args);
expect(staticAssets.getPluginAssetHref('dolly', '/path/for/something.svg/')).toEqual(
'/base-path/plugins/dolly/assets/path/for/something.svg'
);
});
it('removes leading and trailing slash from the assetPath when CDN is configured', () => {
args.cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' });
staticAssets = new StaticAssets(args);
expect(staticAssets.getPluginAssetHref('dolly', '/path/for/something.svg/')).toEqual(
'https://cdn.example.com/test/plugins/dolly/assets/path/for/something.svg'
);
});
});
describe('with a SHA digest provided', () => {
describe('cdn', () => {
it.each([
['https://cdn.example.com', 'https://cdn.example.com/beef', undefined],
['https://cdn.example.com:1234', 'https://cdn.example.com:1234/beef', undefined],
[
'https://cdn.example.com:1234/roast',
'https://cdn.example.com:1234/roast/beef',
undefined,
],
// put slashes around shaDigest
[
'https://cdn.example.com:1234/roast-slash',
'https://cdn.example.com:1234/roast-slash/beef',
'/beef/',
],
])('suffixes the digest to the CDNs path value (%s)', (url, expectedHref, shaDigest) => {
args.shaDigest = shaDigest ?? 'beef';
args.cdnConfig = CdnConfig.from({ url });
staticAssets = new StaticAssets(args);
expect(staticAssets.getHrefBase()).toEqual(expectedHref);
});
});
describe('base path', () => {
it.each([
['', '/beef', undefined],
['/', '/beef', undefined],
['/roast', '/roast/beef', undefined],
['/roast/', '/roast/beef', '/beef/'], // cheeky test adding a slashes to digest
])('suffixes the digest to the server base path "%s")', (url, expectedPath, shaDigest) => {
basePath = new BasePath(url);
args.basePath = basePath;
args.shaDigest = shaDigest ?? 'beef';
staticAssets = new StaticAssets(args);
expect(staticAssets.getHrefBase()).toEqual(expectedPath);
});
});
});
describe('#getPluginServerPath()', () => {
it('provides the path plugin assets can use for server routes', () => {
args.shaDigest = '1234';
staticAssets = new StaticAssets(args);
expect(staticAssets.getPluginServerPath('myPlugin', '/fun/times')).toEqual(
'/1234/plugins/myPlugin/assets/fun/times'
);
});
});
describe('#prependPublicUrl()', () => {
it('with a CDN it appends as expected', () => {
args.cdnConfig = CdnConfig.from({ url: 'http://cdn.example.com/cool?123=true' });
staticAssets = new StaticAssets(args);
expect(staticAssets.prependPublicUrl('beans')).toEqual(
'http://cdn.example.com/cool/beans?123=true'
);
});
it('without a CDN it appends as expected', () => {
staticAssets = new StaticAssets(args);
expect(staticAssets.prependPublicUrl('/cool/beans')).toEqual('/base-path/cool/beans');
});
});
});

View file

@ -0,0 +1,103 @@
/*
* 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 type { BasePath } from '../base_path_service';
import { CdnConfig } from '../cdn_config';
import {
suffixPathnameToPathname,
suffixPathnameToURLPathname,
removeSurroundingSlashes,
} from './util';
export interface InternalStaticAssets {
getHrefBase(): string;
/**
* Intended for use by server code rendering UI or generating links to static assets
* that will ultimately be called from the browser and must respect settings like
* serverBasePath
*/
getPluginAssetHref(pluginName: string, assetPath: string): string;
/**
* Intended for use by server code wanting to register static assets against Kibana
* as server paths
*/
getPluginServerPath(pluginName: string, assetPath: string): string;
/**
* Similar to getPluginServerPath, but not plugin-scoped
*/
prependServerPath(pathname: string): string;
/**
* Will append the given path segment to the configured public path.
*
* @note This could return a path or full URL depending on whether a CDN is configured.
*/
prependPublicUrl(pathname: string): string;
}
/** @internal */
export interface StaticAssetsParams {
basePath: BasePath;
cdnConfig: CdnConfig;
shaDigest: string;
}
/**
* Convention is for trailing slashes in pathnames are stripped.
*/
export class StaticAssets implements InternalStaticAssets {
private readonly assetsHrefBase: string;
private readonly assetsServerPathBase: string;
private readonly hasCdnHost: boolean;
constructor({ basePath, cdnConfig, shaDigest }: StaticAssetsParams) {
const cdnBaseHref = cdnConfig.baseHref;
if (cdnBaseHref) {
this.hasCdnHost = true;
this.assetsHrefBase = suffixPathnameToURLPathname(cdnBaseHref, shaDigest);
} else {
this.hasCdnHost = false;
this.assetsHrefBase = suffixPathnameToPathname(basePath.serverBasePath, shaDigest);
}
this.assetsServerPathBase = `/${shaDigest}`;
}
/**
* Returns a href (hypertext reference) intended to be used as the base for constructing
* other hrefs to static assets.
*/
public getHrefBase(): string {
return this.assetsHrefBase;
}
public getPluginAssetHref(pluginName: string, assetPath: string): string {
if (assetPath.startsWith('/')) {
assetPath = assetPath.slice(1);
}
return `${this.assetsHrefBase}/plugins/${pluginName}/assets/${removeSurroundingSlashes(
assetPath
)}`;
}
public prependServerPath(path: string): string {
return `${this.assetsServerPathBase}/${removeSurroundingSlashes(path)}`;
}
public prependPublicUrl(pathname: string): string {
if (this.hasCdnHost) {
return suffixPathnameToURLPathname(this.assetsHrefBase, pathname);
}
return suffixPathnameToPathname(this.assetsHrefBase, pathname);
}
public getPluginServerPath(pluginName: string, assetPath: string): string {
return `${this.assetsServerPathBase}/plugins/${pluginName}/assets/${removeSurroundingSlashes(
assetPath
)}`;
}
}

View file

@ -0,0 +1,45 @@
/*
* 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 { URL, format } from 'node:url';
function isEmptyPathname(pathname: string): boolean {
return !pathname || pathname === '/';
}
function removeTailSlashes(pathname: string): string {
return pathname.replace(/\/+$/, '');
}
function removeLeadSlashes(pathname: string): string {
return pathname.replace(/^\/+/, '');
}
export function removeSurroundingSlashes(pathname: string): string {
return removeLeadSlashes(removeTailSlashes(pathname));
}
export function suffixPathnameToURLPathname(urlString: string, pathname: string): string {
const url = new URL(urlString);
url.pathname = suffixPathnameToPathname(url.pathname, pathname);
return format(url);
}
/**
* Appends a value to pathname. Pathname is assumed to come from URL.pathname
* Also do some quality control on the path to ensure that it matches URL.pathname.
*/
export function suffixPathnameToPathname(pathnameA: string, pathnameB: string): string {
if (isEmptyPathname(pathnameA)) {
return `/${removeSurroundingSlashes(pathnameB)}`;
}
if (isEmptyPathname(pathnameB)) {
return `/${removeSurroundingSlashes(pathnameA)}`;
}
return `/${removeSurroundingSlashes(pathnameA)}/${removeSurroundingSlashes(pathnameB)}`;
}

View file

@ -33,6 +33,7 @@
"@kbn/core-execution-context-server-mocks",
"@kbn/core-http-context-server-mocks",
"@kbn/logging-mocks",
"@kbn/core-base-server-mocks",
],
"exclude": [
"target/**/*",

View file

@ -87,8 +87,11 @@ const createInternalStaticAssetsMock = (
basePath: BasePathMocked,
cdnUrl: undefined | string = undefined
): InternalStaticAssetsMocked => ({
getHrefBase: jest.fn(() => cdnUrl ?? basePath.serverBasePath),
getHrefBase: jest.fn().mockReturnValue(cdnUrl ?? basePath.serverBasePath),
getPluginAssetHref: jest.fn().mockReturnValue(cdnUrl ?? basePath.serverBasePath),
getPluginServerPath: jest.fn((v, _) => v),
prependServerPath: jest.fn((v) => v),
prependPublicUrl: jest.fn((v) => v),
});
const createAuthMock = () => {
@ -212,6 +215,7 @@ const createSetupContractMock = <
getServerInfo: internalMock.getServerInfo,
staticAssets: {
getPluginAssetHref: jest.fn().mockImplementation((assetPath: string) => assetPath),
prependPublicUrl: jest.fn().mockImplementation((pathname: string) => pathname),
},
};
@ -227,6 +231,7 @@ const createStartContractMock = () => {
getServerInfo: jest.fn(),
staticAssets: {
getPluginAssetHref: jest.fn().mockImplementation((assetPath: string) => assetPath),
prependPublicUrl: jest.fn().mockImplementation((pathname: string) => pathname),
},
};

View file

@ -23,4 +23,23 @@ export interface IStaticAssets {
* ```
*/
getPluginAssetHref(assetPath: string): string;
/**
* Will return an href, either a path for or full URL with the provided path
* appended to the static assets public base path.
*
* Useful for instances were you need to render your own HTML page and link to
* certain static assets.
*
* @example
* ```ts
* // I want to retrieve the href for Kibana's favicon, requires knowledge of path:
* const favIconHref = core.http.statisAssets.prependPublicUrl('/ui/favicons/favicon.svg');
* ```
*
* @note Only use this if you know what you are doing and there is no other option.
* This creates a strong coupling between asset dir structure and your code.
* @param pathname
*/
prependPublicUrl(pathname: string): string;
}

View file

@ -23,6 +23,7 @@ export const createPluginInitializerContextMock = (config: unknown = {}) => {
branch: 'branch',
buildNum: 100,
buildSha: 'buildSha',
buildShaShort: 'buildShaShort',
dist: false,
buildDate: new Date('2023-05-15T23:12:09.000Z'),
buildFlavor: 'traditional',

View file

@ -49,6 +49,7 @@ const createPluginInitializerContextMock = (
branch: 'branch',
buildNum: 100,
buildSha: 'buildSha',
buildShaShort: 'buildShaShort',
dist: false,
buildDate: new Date('2023-05-15T23:12:09.000Z'),
buildFlavor,

View file

@ -19,6 +19,7 @@ const packageInfo: PackageInfo = {
branch: 'master',
buildNum: 1,
buildSha: '',
buildShaShort: '',
version: '7.0.0-alpha1',
dist: false,
buildDate: new Date('2023-05-15T23:12:09.000Z'),

View file

@ -236,6 +236,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>({
registerOnPreResponse: deps.http.registerOnPreResponse,
basePath: deps.http.basePath,
staticAssets: {
prependPublicUrl: (pathname: string) => deps.http.staticAssets.prependPublicUrl(pathname),
getPluginAssetHref: (assetPath: string) =>
deps.http.staticAssets.getPluginAssetHref(plugin.name, assetPath),
},
@ -329,6 +330,7 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>({
basePath: deps.http.basePath,
getServerInfo: deps.http.getServerInfo,
staticAssets: {
prependPublicUrl: (pathname: string) => deps.http.staticAssets.prependPublicUrl(pathname),
getPluginAssetHref: (assetPath: string) =>
deps.http.staticAssets.getPluginAssetHref(plugin.name, assetPath),
},

View file

@ -1165,8 +1165,10 @@ describe('PluginsService', () => {
});
describe('plugin initialization', () => {
let prebootPlugins: PluginWrapper[];
let standardPlugins: PluginWrapper[];
beforeEach(() => {
const prebootPlugins = [
prebootPlugins = [
createPlugin('plugin-1-preboot', {
type: PluginType.preboot,
path: 'path-1-preboot',
@ -1178,7 +1180,7 @@ describe('PluginsService', () => {
version: 'version-2',
}),
];
const standardPlugins = [
standardPlugins = [
createPlugin('plugin-1-standard', {
path: 'path-1-standard',
version: 'version-1',
@ -1299,6 +1301,31 @@ describe('PluginsService', () => {
expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
});
it('#preboot registers expected static dirs', async () => {
prebootDeps.http.staticAssets.getPluginServerPath.mockImplementation(
(pluginName: string) => `/static-assets/${pluginName}`
);
await pluginsService.discover({ environment: environmentPreboot, node: nodePreboot });
await pluginsService.preboot(prebootDeps);
expect(prebootDeps.http.registerStaticDir).toHaveBeenCalledTimes(prebootPlugins.length * 2);
expect(prebootDeps.http.registerStaticDir).toHaveBeenCalledWith(
'/static-assets/plugin-1-preboot',
expect.any(String)
);
expect(prebootDeps.http.registerStaticDir).toHaveBeenCalledWith(
'/plugins/plugin-1-preboot/assets/{path*}',
expect.any(String)
);
expect(prebootDeps.http.registerStaticDir).toHaveBeenCalledWith(
'/static-assets/plugin-2-preboot',
expect.any(String)
);
expect(prebootDeps.http.registerStaticDir).toHaveBeenCalledWith(
'/plugins/plugin-2-preboot/assets/{path*}',
expect.any(String)
);
});
it('#setup does initialize `standard` plugins if plugins.initialize is true', async () => {
config$.next({ plugins: { initialize: true } });
await pluginsService.discover({ environment: environmentPreboot, node: nodePreboot });
@ -1319,6 +1346,32 @@ describe('PluginsService', () => {
expect(prebootMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
expect(initialized).toBe(false);
});
it('#setup registers expected static dirs', async () => {
await pluginsService.discover({ environment: environmentPreboot, node: nodePreboot });
await pluginsService.preboot(prebootDeps);
setupDeps.http.staticAssets.getPluginServerPath.mockImplementation(
(pluginName: string) => `/static-assets/${pluginName}`
);
await pluginsService.setup(setupDeps);
expect(setupDeps.http.registerStaticDir).toHaveBeenCalledTimes(standardPlugins.length * 2);
expect(setupDeps.http.registerStaticDir).toHaveBeenCalledWith(
'/static-assets/plugin-1-standard',
expect.any(String)
);
expect(setupDeps.http.registerStaticDir).toHaveBeenCalledWith(
'/plugins/plugin-1-standard/assets/{path*}',
expect.any(String)
);
expect(setupDeps.http.registerStaticDir).toHaveBeenCalledWith(
'/static-assets/plugin-2-standard',
expect.any(String)
);
expect(setupDeps.http.registerStaticDir).toHaveBeenCalledWith(
'/plugins/plugin-2-standard/assets/{path*}',
expect.any(String)
);
});
});
describe('#getExposedPluginConfigsToUsage', () => {

View file

@ -448,10 +448,16 @@ export class PluginsService
uiPluginInternalInfo: Map<PluginName, InternalPluginInfo>
) {
for (const [pluginName, pluginInfo] of uiPluginInternalInfo) {
deps.http.registerStaticDir(
/**
* Serve UI from sha-scoped and not-sha-scoped paths to allow time for plugin code to migrate
* Eventually we only want to serve from the sha scoped path
*/
[
deps.http.staticAssets.getPluginServerPath(pluginName, '{path*}'),
`/plugins/${pluginName}/assets/{path*}`,
pluginInfo.publicAssetsDir
);
].forEach((path) => {
deps.http.registerStaticDir(path, pluginInfo.publicAssetsDir);
});
}
}
}

View file

@ -24,6 +24,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -92,6 +93,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -156,6 +158,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -224,6 +227,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -288,6 +292,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -352,6 +357,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -420,6 +426,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -484,6 +491,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -553,6 +561,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -621,6 +630,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -690,6 +700,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -763,6 +774,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -827,6 +839,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -896,6 +909,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -969,6 +983,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},
@ -1038,6 +1053,7 @@ Object {
"buildFlavor": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"buildShaShort": "XXXXXX",
"dist": Any<Boolean>,
"version": Any<String>,
},

View file

@ -25,6 +25,7 @@ const createPackageInfo = (parts: Partial<PackageInfo> = {}): PackageInfo => ({
branch: 'master',
buildNum: 42,
buildSha: 'buildSha',
buildShaShort: 'buildShaShort',
buildDate: new Date('2023-05-15T23:12:09.000Z'),
dist: false,
version: '8.0.0',
@ -62,7 +63,7 @@ describe('bootstrapRenderer', () => {
auth,
packageInfo,
uiPlugins,
baseHref: '/base-path',
baseHref: `/base-path/${packageInfo.buildShaShort}`, // the base href as provided by static assets module
});
});
@ -319,7 +320,7 @@ describe('bootstrapRenderer', () => {
expect(getPluginsBundlePathsMock).toHaveBeenCalledWith({
isAnonymousPage,
uiPlugins,
bundlesHref: '/base-path/42/bundles',
bundlesHref: '/base-path/buildShaShort/bundles',
});
});
});
@ -338,7 +339,7 @@ describe('bootstrapRenderer', () => {
expect(getJsDependencyPathsMock).toHaveBeenCalledTimes(1);
expect(getJsDependencyPathsMock).toHaveBeenCalledWith(
'/base-path/42/bundles',
'/base-path/buildShaShort/bundles',
pluginsBundlePaths
);
});

View file

@ -79,8 +79,7 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({
themeVersion,
darkMode,
});
const buildHash = packageInfo.buildNum;
const bundlesHref = getBundlesHref(baseHref, String(buildHash));
const bundlesHref = getBundlesHref(baseHref);
const bundlePaths = getPluginsBundlePaths({
uiPlugins,

View file

@ -16,14 +16,14 @@ describe('getStylesheetPaths', () => {
getStylesheetPaths({
darkMode: true,
themeVersion: 'v8',
baseHref: '/base-path',
baseHref: '/base-path/buildShaShort',
buildNum: 17,
})
).toMatchInlineSnapshot(`
Array [
"/base-path/17/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.dark.css",
"/base-path/17/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css",
"/base-path/ui/legacy_dark_theme.min.css",
"/base-path/buildShaShort/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.dark.css",
"/base-path/buildShaShort/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css",
"/base-path/buildShaShort/ui/legacy_dark_theme.min.css",
]
`);
});
@ -36,14 +36,14 @@ describe('getStylesheetPaths', () => {
getStylesheetPaths({
darkMode: false,
themeVersion: 'v8',
baseHref: '/base-path',
baseHref: '/base-path/buildShaShort',
buildNum: 69,
})
).toMatchInlineSnapshot(`
Array [
"/base-path/69/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css",
"/base-path/69/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css",
"/base-path/ui/legacy_light_theme.min.css",
"/base-path/buildShaShort/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css",
"/base-path/buildShaShort/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css",
"/base-path/buildShaShort/ui/legacy_light_theme.min.css",
]
`);
});

View file

@ -22,8 +22,7 @@ export const getSettingValue = <T>(
return convert(value);
};
export const getBundlesHref = (baseHref: string, buildNr: string): string =>
`${baseHref}/${buildNr}/bundles`;
export const getBundlesHref = (baseHref: string): string => `${baseHref}/bundles`;
export const getStylesheetPaths = ({
themeVersion,
@ -36,7 +35,7 @@ export const getStylesheetPaths = ({
buildNum: number;
baseHref: string;
}) => {
const bundlesHref = getBundlesHref(baseHref, String(buildNum));
const bundlesHref = getBundlesHref(baseHref);
return [
...(darkMode
? [

View file

@ -32,6 +32,7 @@ Env {
"buildFlavor": "traditional",
"buildNum": 9007199254740991,
"buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"buildShaShort": "XXXXXXXXXXXX",
"dist": false,
"version": "v1",
},
@ -75,6 +76,7 @@ Env {
"buildFlavor": "traditional",
"buildNum": 9007199254740991,
"buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"buildShaShort": "XXXXXXXXXXXX",
"dist": false,
"version": "v1",
},
@ -117,6 +119,7 @@ Env {
"buildFlavor": "traditional",
"buildNum": 9007199254740991,
"buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"buildShaShort": "XXXXXXXXXXXX",
"dist": false,
"version": "some-version",
},
@ -159,6 +162,7 @@ Env {
"buildFlavor": "traditional",
"buildNum": 100,
"buildSha": "feature-v1-build-sha",
"buildShaShort": "feature-v1-b",
"dist": true,
"version": "v1",
},
@ -201,6 +205,7 @@ Env {
"buildFlavor": "traditional",
"buildNum": 9007199254740991,
"buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"buildShaShort": "XXXXXXXXXXXX",
"dist": false,
"version": "v1",
},
@ -243,6 +248,7 @@ Env {
"buildFlavor": "traditional",
"buildNum": 100,
"buildSha": "feature-v1-build-sha",
"buildShaShort": "feature-v1-b",
"dist": true,
"version": "v1",
},

View file

@ -248,3 +248,35 @@ describe('packageInfo.buildFlavor', () => {
expect(env.packageInfo.buildFlavor).toEqual('traditional');
});
});
describe('packageInfo.buildShaShort', () => {
const sha = 'c6e1a25bea71a623929a8f172c0273bf0c811ca0';
it('provides the sha and a short version of the sha', () => {
mockPackage.raw = {
branch: 'some-branch',
version: 'some-version',
};
const env = new Env(
'/some/home/dir',
{
branch: 'whathaveyou',
version: 'v1',
build: {
distributable: true,
number: 100,
sha,
date: BUILD_DATE,
},
},
getEnvOptions({
cliArgs: { dev: false },
configs: ['/some/other/path/some-kibana.yml'],
repoPackages: ['FakePackage1', 'FakePackage2'] as unknown as Package[],
})
);
expect(env.packageInfo.buildSha).toEqual('c6e1a25bea71a623929a8f172c0273bf0c811ca0');
expect(env.packageInfo.buildShaShort).toEqual('c6e1a25bea71');
});
});

View file

@ -121,6 +121,7 @@ export class Env {
branch: pkg.branch,
buildNum: isKibanaDistributable ? pkg.build.number : Number.MAX_SAFE_INTEGER,
buildSha: isKibanaDistributable ? pkg.build.sha : 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
buildShaShort: isKibanaDistributable ? pkg.build.sha.slice(0, 12) : 'XXXXXXXXXXXX',
version: pkg.version,
dist: isKibanaDistributable,
buildDate: isKibanaDistributable ? new Date(pkg.build.date) : new Date(),

View file

@ -14,6 +14,7 @@ export interface PackageInfo {
branch: string;
buildNum: number;
buildSha: string;
buildShaShort: string;
buildDate: Date;
buildFlavor: BuildFlavor;
dist: boolean;

View file

@ -17,7 +17,7 @@ import { HttpService } from '@kbn/core-http-server-internal';
import { createHttpServer } from '@kbn/core-http-server-mocks';
import { registerRouteForBundle, FileHashCache } from '@kbn/core-apps-server-internal';
const buildNum = 1234;
const buildHash = 'buildHash';
const fooPluginFixture = resolve(__dirname, './__fixtures__/plugin/foo');
describe('bundle routes', () => {
@ -47,8 +47,8 @@ describe('bundle routes', () => {
isDist,
fileHashCache,
bundlesPath: fooPluginFixture,
routePath: `/${buildNum}/bundles/plugin/foo/`,
publicPath: `/${buildNum}/bundles/plugin/foo/`,
routePath: `/${buildHash}/bundles/plugin/foo/`,
publicPath: `/${buildHash}/bundles/plugin/foo/`,
});
};
@ -62,7 +62,7 @@ describe('bundle routes', () => {
await server.start();
const response = await supertest(innerServer.listener)
.get(`/${buildNum}/bundles/plugin/foo/image.png`)
.get(`/${buildHash}/bundles/plugin/foo/image.png`)
.expect(200);
const actualImage = await readFile(resolve(fooPluginFixture, 'image.png'));
@ -80,7 +80,7 @@ describe('bundle routes', () => {
await server.start();
const response = await supertest(innerServer.listener)
.get(`/${buildNum}/bundles/plugin/foo/plugin.js`)
.get(`/${buildHash}/bundles/plugin/foo/plugin.js`)
.expect(200);
const actualFile = await readFile(resolve(fooPluginFixture, 'plugin.js'));
@ -98,7 +98,7 @@ describe('bundle routes', () => {
await server.start();
await supertest(innerServer.listener)
.get(`/${buildNum}/bundles/plugin/foo/../outside_output.js`)
.get(`/${buildHash}/bundles/plugin/foo/../outside_output.js`)
.expect(404);
});
@ -112,7 +112,7 @@ describe('bundle routes', () => {
await server.start();
await supertest(innerServer.listener)
.get(`/${buildNum}/bundles/plugin/foo/missing.js`)
.get(`/${buildHash}/bundles/plugin/foo/missing.js`)
.expect(404);
});
@ -126,7 +126,7 @@ describe('bundle routes', () => {
await server.start();
const response = await supertest(innerServer.listener)
.get(`/${buildNum}/bundles/plugin/foo/gzip_chunk.js`)
.get(`/${buildHash}/bundles/plugin/foo/gzip_chunk.js`)
.expect(200);
expect(response.get('content-encoding')).toEqual('gzip');
@ -151,7 +151,7 @@ describe('bundle routes', () => {
await server.start();
const response = await supertest(innerServer.listener)
.get(`/${buildNum}/bundles/plugin/foo/gzip_chunk.js`)
.get(`/${buildHash}/bundles/plugin/foo/gzip_chunk.js`)
.expect(200);
expect(response.get('cache-control')).toEqual('max-age=31536000');
@ -170,7 +170,7 @@ describe('bundle routes', () => {
await server.start();
const response = await supertest(innerServer.listener)
.get(`/${buildNum}/bundles/plugin/foo/gzip_chunk.js`)
.get(`/${buildHash}/bundles/plugin/foo/gzip_chunk.js`)
.expect(200);
expect(response.get('cache-control')).toEqual('must-revalidate');

View file

@ -11,19 +11,21 @@ import supertest from 'supertest';
import moment from 'moment';
import { of } from 'rxjs';
import { ByteSizeValue } from '@kbn/config-schema';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { Router } from '@kbn/core-http-router-server-internal';
import { HttpServer, HttpConfig } from '@kbn/core-http-server-internal';
import { mockCoreContext } from '@kbn/core-base-server-mocks';
import type { Logger } from '@kbn/logging';
describe('Http server', () => {
let server: HttpServer;
let config: HttpConfig;
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
let logger: Logger;
let coreContext: ReturnType<typeof mockCoreContext.create>;
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
beforeEach(() => {
const loggingService = loggingSystemMock.create();
logger = loggingSystemMock.createLogger();
coreContext = mockCoreContext.create();
logger = coreContext.logger.get();
config = {
name: 'kibana',
@ -43,7 +45,7 @@ describe('Http server', () => {
shutdownTimeout: moment.duration(5, 's'),
} as any;
server = new HttpServer(loggingService, 'tests', of(config.shutdownTimeout));
server = new HttpServer(coreContext, 'tests', of(config.shutdownTimeout));
});
describe('Graceful shutdown', () => {

View file

@ -10,7 +10,6 @@ import supertest from 'supertest';
import { duration } from 'moment';
import { BehaviorSubject, of } from 'rxjs';
import { KBN_CERT_PATH, KBN_KEY_PATH, ES_KEY_PATH, ES_CERT_PATH } from '@kbn/dev-utils';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { Router } from '@kbn/core-http-router-server-internal';
import {
HttpServer,
@ -20,6 +19,8 @@ import {
externalUrlConfig,
} from '@kbn/core-http-server-internal';
import { isServerTLS, flattenCertificateChain, fetchPeerCertificate } from './tls_utils';
import { mockCoreContext } from '@kbn/core-base-server-mocks';
import type { Logger } from '@kbn/logging';
const CSP_CONFIG = cspConfig.schema.validate({});
const EXTERNAL_URL_CONFIG = externalUrlConfig.schema.validate({});
@ -27,16 +28,16 @@ const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
describe('HttpServer - TLS config', () => {
let server: HttpServer;
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
let logger: Logger;
beforeAll(() => {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
});
beforeEach(() => {
const loggingService = loggingSystemMock.create();
logger = loggingSystemMock.createLogger();
server = new HttpServer(loggingService, 'tests', of(duration('1s')));
const coreContext = mockCoreContext.create();
logger = coreContext.logger.get();
server = new HttpServer(coreContext, 'tests', of(duration('1s')));
});
it('supports dynamic reloading of the TLS configuration', async () => {

View file

@ -83,6 +83,7 @@ describe('GET /api/status', () => {
branch: 'xbranch',
buildNum: 1234,
buildSha: 'xsha',
buildShaShort: 'x',
dist: true,
version: '9.9.9-SNAPSHOT',
buildDate: new Date('2023-05-15T23:12:09.000Z'),

View file

@ -98,6 +98,7 @@ function pluginInitializerContextMock<T>(config: T = {} as T) {
branch: 'branch',
buildNum: 100,
buildSha: 'buildSha',
buildShaShort: 'buildShaShort',
dist: false,
buildDate: new Date('2023-05-15T23:12:09.000Z'),
buildFlavor: 'traditional',

View file

@ -157,6 +157,7 @@
"@kbn/core-plugins-contracts-server",
"@kbn/dev-utils",
"@kbn/server-http-tools",
"@kbn/core-base-server-mocks",
],
"exclude": [
"target/**/*",

View file

@ -14,38 +14,38 @@ export default function ({ getService }) {
const supertest = getService('supertest');
describe('bundle compression', function () {
let buildNum;
let buildHash;
before(async () => {
const resp = await supertest.get('/api/status').expect(200);
buildNum = resp.body.version.build_number;
buildHash = resp.body.version.build_hash.slice(0, 12);
});
it('returns gzip files when client only supports gzip', () =>
supertest
// We use the kbn-ui-shared-deps for these tests since they are always built with br compressed outputs,
// even in dev. Bundles built by @kbn/optimizer are only built with br compression in dist mode.
.get(`/${buildNum}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`)
.get(`/${buildHash}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`)
.set('Accept-Encoding', 'gzip')
.expect(200)
.expect('Content-Encoding', 'gzip'));
it('returns br files when client only supports br', () =>
supertest
.get(`/${buildNum}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`)
.get(`/${buildHash}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`)
.set('Accept-Encoding', 'br')
.expect(200)
.expect('Content-Encoding', 'br'));
it('returns br files when client only supports gzip and br', () =>
supertest
.get(`/${buildNum}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`)
.get(`/${buildHash}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`)
.set('Accept-Encoding', 'gzip, br')
.expect(200)
.expect('Content-Encoding', 'br'));
it('returns gzip files when client prefers gzip', () =>
supertest
.get(`/${buildNum}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`)
.get(`/${buildHash}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`)
.set('Accept-Encoding', 'gzip;q=1.0, br;q=0.5')
.expect(200)
.expect('Content-Encoding', 'gzip'));

View file

@ -77,7 +77,10 @@ export class CloudFullStoryPlugin implements Plugin {
...(pageVarsDebounceTime
? { pageVarsDebounceTimeMs: duration(pageVarsDebounceTime).asMilliseconds() }
: {}),
// Load an Elastic-internally audited script. Ideally, it should be hosted on a CDN.
/**
* FIXME: this should use the {@link IStaticAssets['getPluginAssetHref']}
* function. Then we can avoid registering our own endpoint in this plugin.
*/
scriptUrl: basePath.prepend(
`/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/fullstory.js`
),

View file

@ -33,6 +33,7 @@ describe('PdfMaker', () => {
branch: 'screenshot-test',
buildNum: 567891011,
buildSha: 'screenshot-dfdfed0a',
buildShaShort: 'scr-dfdfed0a',
dist: false,
version: '1000.0.0',
buildDate: new Date('2023-05-15T23:12:09.000Z'),

View file

@ -56,6 +56,7 @@ describe('Screenshot Observable Pipeline', () => {
branch: 'screenshot-test',
buildNum: 567891011,
buildSha: 'screenshot-dfdfed0a',
buildShaShort: 'scrn-dfdfed0a',
dist: false,
version: '5000.0.0',
buildDate: new Date('2023-05-15T23:12:09.000Z'),

View file

@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PromptPage renders as expected with additional scripts 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/some/script1.js\\"></script><script src=\\"/mock-basepath/some/script2.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">Some Title</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><div>Some Body</div></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><span>Action#1</span></div><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><span>Action#2</span></div></div></div></div></div></div></div></section></main></div></body></html>"`;
exports[`PromptPage renders as expected with additional scripts 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/some/script1.js\\"></script><script src=\\"/mock-basepath/some/script2.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">Some Title</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><div>Some Body</div></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><span>Action#1</span></div><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><span>Action#2</span></div></div></div></div></div></div></div></section></main></div></body></html>"`;
exports[`PromptPage renders as expected without additional scripts 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">Some Title</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><div>Some Body</div></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><span>Action#1</span></div><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><span>Action#2</span></div></div></div></div></div></div></div></section></main></div></body></html>"`;
exports[`PromptPage renders as expected without additional scripts 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">Some Title</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><div>Some Body</div></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><span>Action#1</span></div><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><span>Action#2</span></div></div></div></div></div></div></div></section></main></div></body></html>"`;

View file

@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UnauthenticatedPage renders as expected 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">We hit an authentication error</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><p>Try logging in again, and if the problem persists, contact your system administrator.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/some/url?some-query=some-value#some-hash\\" rel=\\"noreferrer\\" class=\\"euiButton eui-8utmkn-euiButtonDisplay-m-defaultMinWidth-fill-primary\\" data-test-subj=\\"logInButton\\"><span class=\\"eui-1bascr2-euiButtonDisplayContent\\">Log in</span></a></div></div></div></div></div></div></div></section></main></div></body></html>"`;
exports[`UnauthenticatedPage renders as expected 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">We hit an authentication error</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><p>Try logging in again, and if the problem persists, contact your system administrator.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/some/url?some-query=some-value#some-hash\\" rel=\\"noreferrer\\" class=\\"euiButton eui-8utmkn-euiButtonDisplay-m-defaultMinWidth-fill-primary\\" data-test-subj=\\"logInButton\\"><span class=\\"eui-1bascr2-euiButtonDisplayContent\\">Log in</span></a></div></div></div></div></div></div></div></section></main></div></body></html>"`;
exports[`UnauthenticatedPage renders as expected with custom title 1`] = `"<html lang=\\"en\\"><head><title>My Company Name</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">We hit an authentication error</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><p>Try logging in again, and if the problem persists, contact your system administrator.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/some/url?some-query=some-value#some-hash\\" rel=\\"noreferrer\\" class=\\"euiButton eui-8utmkn-euiButtonDisplay-m-defaultMinWidth-fill-primary\\" data-test-subj=\\"logInButton\\"><span class=\\"eui-1bascr2-euiButtonDisplayContent\\">Log in</span></a></div></div></div></div></div></div></div></section></main></div></body></html>"`;
exports[`UnauthenticatedPage renders as expected with custom title 1`] = `"<html lang=\\"en\\"><head><title>My Company Name</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">We hit an authentication error</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><p>Try logging in again, and if the problem persists, contact your system administrator.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/some/url?some-query=some-value#some-hash\\" rel=\\"noreferrer\\" class=\\"euiButton eui-8utmkn-euiButtonDisplay-m-defaultMinWidth-fill-primary\\" data-test-subj=\\"logInButton\\"><span class=\\"eui-1bascr2-euiButtonDisplayContent\\">Log in</span></a></div></div></div></div></div></div></div></section></main></div></body></html>"`;

View file

@ -19,6 +19,7 @@ import type {
ElasticsearchServiceSetup,
HttpServiceSetup,
HttpServiceStart,
IStaticAssets,
KibanaRequest,
Logger,
LoggerFactory,
@ -63,7 +64,7 @@ describe('AuthenticationService', () => {
elasticsearch: jest.Mocked<ElasticsearchServiceSetup>;
config: ConfigType;
license: jest.Mocked<SecurityLicense>;
buildNumber: number;
staticAssets: IStaticAssets;
customBranding: jest.Mocked<CustomBrandingSetup>;
};
let mockStartAuthenticationParams: {
@ -96,7 +97,7 @@ describe('AuthenticationService', () => {
isTLSEnabled: false,
}),
license: licenseMock.create(),
buildNumber: 100500,
staticAssets: coreSetupMock.http.staticAssets,
customBranding: customBrandingServiceMock.createSetupContract(),
};
mockCanRedirectRequest.mockReturnValue(false);
@ -983,7 +984,7 @@ describe('AuthenticationService', () => {
expect(mockRenderUnauthorizedPage).toHaveBeenCalledWith({
basePath: mockSetupAuthenticationParams.http.basePath,
buildNumber: 100500,
staticAssets: expect.any(Object),
originalURL: '/mock-server-basepath/app/some',
});
});
@ -1015,7 +1016,7 @@ describe('AuthenticationService', () => {
expect(mockRenderUnauthorizedPage).toHaveBeenCalledWith({
basePath: mockSetupAuthenticationParams.http.basePath,
buildNumber: 100500,
staticAssets: expect.any(Object),
originalURL: '/mock-server-basepath/app/some',
});
});
@ -1050,7 +1051,7 @@ describe('AuthenticationService', () => {
expect(mockRenderUnauthorizedPage).toHaveBeenCalledWith({
basePath: mockSetupAuthenticationParams.http.basePath,
buildNumber: 100500,
staticAssets: expect.any(Object),
originalURL: '/mock-server-basepath/',
});
});

View file

@ -40,12 +40,14 @@ import type { Session } from '../session_management';
import type { UserProfileServiceStartInternal } from '../user_profile';
interface AuthenticationServiceSetupParams {
http: Pick<HttpServiceSetup, 'basePath' | 'csp' | 'registerAuth' | 'registerOnPreResponse'>;
http: Pick<
HttpServiceSetup,
'basePath' | 'csp' | 'registerAuth' | 'registerOnPreResponse' | 'staticAssets'
>;
customBranding: CustomBrandingSetup;
elasticsearch: Pick<ElasticsearchServiceSetup, 'setUnauthorizedErrorHandler'>;
config: ConfigType;
license: SecurityLicense;
buildNumber: number;
}
interface AuthenticationServiceStartParams {
@ -92,7 +94,6 @@ export class AuthenticationService {
config,
http,
license,
buildNumber,
elasticsearch,
customBranding,
}: AuthenticationServiceSetupParams) {
@ -204,8 +205,8 @@ export class AuthenticationService {
});
return toolkit.render({
body: renderUnauthenticatedPage({
buildNumber,
basePath: http.basePath,
staticAssets: http.staticAssets,
originalURL,
customBranding: customBrandingValue,
}),

View file

@ -26,7 +26,7 @@ describe('UnauthenticatedPage', () => {
const body = renderToStaticMarkup(
<UnauthenticatedPage
originalURL="/some/url?some-query=some-value#some-hash"
buildNumber={100500}
staticAssets={mockCoreSetup.http.staticAssets}
basePath={mockCoreSetup.http.basePath}
customBranding={{}}
/>
@ -44,7 +44,7 @@ describe('UnauthenticatedPage', () => {
const body = renderToStaticMarkup(
<UnauthenticatedPage
originalURL="/some/url?some-query=some-value#some-hash"
buildNumber={100500}
staticAssets={mockCoreSetup.http.staticAssets}
basePath={mockCoreSetup.http.basePath}
customBranding={{ pageTitle: 'My Company Name' }}
/>

View file

@ -11,6 +11,7 @@ import { renderToStaticMarkup } from 'react-dom/server';
import type { IBasePath } from '@kbn/core/server';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
import type { IStaticAssets } from '@kbn/core-http-server';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@ -18,16 +19,21 @@ import { PromptPage } from '../prompt_page';
interface Props {
originalURL: string;
buildNumber: number;
basePath: IBasePath;
staticAssets: IStaticAssets;
customBranding: CustomBranding;
}
export function UnauthenticatedPage({ basePath, originalURL, buildNumber, customBranding }: Props) {
export function UnauthenticatedPage({
basePath,
originalURL,
staticAssets,
customBranding,
}: Props) {
return (
<PromptPage
buildNumber={buildNumber}
basePath={basePath}
staticAssets={staticAssets}
title={i18n.translate('xpack.security.unauthenticated.pageTitle', {
defaultMessage: 'We hit an authentication error',
})}

View file

@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResetSessionPage renders as expected 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/internal/security/reset_session_page.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">You do not have permission to access the requested page</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><p>Either go back to the previous page or log in as a different user.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/path/to/logout\\" rel=\\"noreferrer\\" class=\\"euiButton eui-8utmkn-euiButtonDisplay-m-defaultMinWidth-fill-primary\\" data-test-subj=\\"ResetSessionButton\\"><span class=\\"eui-1bascr2-euiButtonDisplayContent\\">Log in as different user</span></a></div><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><button class=\\"euiButtonEmpty eui-9ywwuh-euiButtonDisplay-euiButtonEmpty-m-empty-primary\\" type=\\"button\\" id=\\"goBackButton\\"><span class=\\"euiButtonEmpty__content eui-1bascr2-euiButtonDisplayContent\\"><span class=\\"eui-textTruncate euiButtonEmpty__text\\">Go back</span></span></button></div></div></div></div></div></div></div></section></main></div></body></html>"`;
exports[`ResetSessionPage renders as expected 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/internal/security/reset_session_page.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">You do not have permission to access the requested page</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><p>Either go back to the previous page or log in as a different user.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/path/to/logout\\" rel=\\"noreferrer\\" class=\\"euiButton eui-8utmkn-euiButtonDisplay-m-defaultMinWidth-fill-primary\\" data-test-subj=\\"ResetSessionButton\\"><span class=\\"eui-1bascr2-euiButtonDisplayContent\\">Log in as different user</span></a></div><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><button class=\\"euiButtonEmpty eui-9ywwuh-euiButtonDisplay-euiButtonEmpty-m-empty-primary\\" type=\\"button\\" id=\\"goBackButton\\"><span class=\\"euiButtonEmpty__content eui-1bascr2-euiButtonDisplayContent\\"><span class=\\"eui-textTruncate euiButtonEmpty__text\\">Go back</span></span></button></div></div></div></div></div></div></div></section></main></div></body></html>"`;
exports[`ResetSessionPage renders as expected with custom page title 1`] = `"<html lang=\\"en\\"><head><title>My Company Name</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/internal/security/reset_session_page.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">You do not have permission to access the requested page</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><p>Either go back to the previous page or log in as a different user.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/path/to/logout\\" rel=\\"noreferrer\\" class=\\"euiButton eui-8utmkn-euiButtonDisplay-m-defaultMinWidth-fill-primary\\" data-test-subj=\\"ResetSessionButton\\"><span class=\\"eui-1bascr2-euiButtonDisplayContent\\">Log in as different user</span></a></div><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><button class=\\"euiButtonEmpty eui-9ywwuh-euiButtonDisplay-euiButtonEmpty-m-empty-primary\\" type=\\"button\\" id=\\"goBackButton\\"><span class=\\"euiButtonEmpty__content eui-1bascr2-euiButtonDisplayContent\\"><span class=\\"eui-textTruncate euiButtonEmpty__text\\">Go back</span></span></button></div></div></div></div></div></div></div></section></main></div></body></html>"`;
exports[`ResetSessionPage renders as expected with custom page title 1`] = `"<html lang=\\"en\\"><head><title>My Company Name</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/internal/security/reset_session_page.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div data-test-subj=\\"promptPage\\" style=\\"min-block-size:max(460px, 100vh);padding-block-start:var(--euiFixedHeadersOffset, 0)\\" class=\\"euiPageTemplate eui-cjgvy1-euiPageOuter-row-grow\\"><main id=\\"EuiPageTemplateInner_generated-id\\" class=\\"eui-nq554q-euiPageInner\\"><section class=\\"eui-68douo-euiPageSection-grow-l-center-transparent\\"><div class=\\"eui-1sghhs8-euiPageSection__content-l-center\\"><div class=\\"euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-12g67tv-euiPanel-m-plain-hasShadow\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"warning\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">You do not have permission to access the requested page</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-eqvwj3-euiText-m-euiTextColor-subdued\\"><p>Either go back to the previous page or log in as a different user.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-12cw070-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/path/to/logout\\" rel=\\"noreferrer\\" class=\\"euiButton eui-8utmkn-euiButtonDisplay-m-defaultMinWidth-fill-primary\\" data-test-subj=\\"ResetSessionButton\\"><span class=\\"eui-1bascr2-euiButtonDisplayContent\\">Log in as different user</span></a></div><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><button class=\\"euiButtonEmpty eui-9ywwuh-euiButtonDisplay-euiButtonEmpty-m-empty-primary\\" type=\\"button\\" id=\\"goBackButton\\"><span class=\\"euiButtonEmpty__content eui-1bascr2-euiButtonDisplayContent\\"><span class=\\"eui-textTruncate euiButtonEmpty__text\\">Go back</span></span></button></div></div></div></div></div></div></div></section></main></div></body></html>"`;

View file

@ -76,7 +76,6 @@ it(`#setup returns exposed services`, () => {
loggers: loggingSystemMock.create(),
kibanaIndexName,
packageVersion: 'some-version',
buildNumber: 42,
features: mockFeaturesSetup,
getSpacesService: mockGetSpacesService,
getCurrentUser: jest.fn(),
@ -138,7 +137,6 @@ describe('#start', () => {
loggers: loggingSystemMock.create(),
kibanaIndexName,
packageVersion: 'some-version',
buildNumber: 42,
features: featuresPluginMock.createSetup(),
getSpacesService: jest
.fn()
@ -211,7 +209,6 @@ it('#stop unsubscribes from license and ES updates.', async () => {
loggers: loggingSystemMock.create(),
kibanaIndexName,
packageVersion: 'some-version',
buildNumber: 42,
features: featuresPluginMock.createSetup(),
getSpacesService: jest
.fn()

View file

@ -57,7 +57,6 @@ export { Actions } from './actions';
interface AuthorizationServiceSetupParams {
packageVersion: string;
buildNumber: number;
http: HttpServiceSetup;
capabilities: CapabilitiesSetup;
getClusterClient: () => Promise<IClusterClient>;
@ -100,7 +99,6 @@ export class AuthorizationService {
http,
capabilities,
packageVersion,
buildNumber,
getClusterClient,
license,
loggers,
@ -179,7 +177,7 @@ export class AuthorizationService {
const next = `${http.basePath.get(request)}${request.url.pathname}${request.url.search}`;
const body = renderToString(
<ResetSessionPage
buildNumber={buildNumber}
staticAssets={http.staticAssets}
basePath={http.basePath}
logoutUrl={http.basePath.prepend(
`/api/security/logout?${querystring.stringify({ next })}`

View file

@ -26,7 +26,7 @@ describe('ResetSessionPage', () => {
const body = renderToStaticMarkup(
<ResetSessionPage
logoutUrl="/path/to/logout"
buildNumber={100500}
staticAssets={mockCoreSetup.http.staticAssets}
basePath={mockCoreSetup.http.basePath}
customBranding={{}}
/>
@ -44,7 +44,7 @@ describe('ResetSessionPage', () => {
const body = renderToStaticMarkup(
<ResetSessionPage
logoutUrl="/path/to/logout"
buildNumber={100500}
staticAssets={mockCoreSetup.http.staticAssets}
basePath={mockCoreSetup.http.basePath}
customBranding={{ pageTitle: 'My Company Name' }}
/>

View file

@ -10,6 +10,7 @@ import React from 'react';
import type { IBasePath } from '@kbn/core/server';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
import type { IStaticAssets } from '@kbn/core-http-server';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@ -22,18 +23,18 @@ import { PromptPage } from '../prompt_page';
*/
export function ResetSessionPage({
logoutUrl,
buildNumber,
staticAssets,
basePath,
customBranding,
}: {
logoutUrl: string;
buildNumber: number;
staticAssets: IStaticAssets;
basePath: IBasePath;
customBranding: CustomBranding;
}) {
return (
<PromptPage
buildNumber={buildNumber}
staticAssets={staticAssets}
basePath={basePath}
scriptPaths={['/internal/security/reset_session_page.js']}
title={i18n.translate('xpack.security.resetSession.title', {

View file

@ -255,7 +255,6 @@ export class SecurityPlugin
elasticsearch: core.elasticsearch,
config,
license,
buildNumber: this.initializerContext.env.packageInfo.buildNum,
customBranding: core.customBranding,
});
@ -283,7 +282,6 @@ export class SecurityPlugin
loggers: this.initializerContext.logger,
kibanaIndexName,
packageVersion: this.initializerContext.env.packageInfo.version,
buildNumber: this.initializerContext.env.packageInfo.buildNum,
getSpacesService: () => spaces?.spacesService,
features,
getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request),

View file

@ -25,7 +25,7 @@ describe('PromptPage', () => {
const body = renderToStaticMarkup(
<PromptPage
buildNumber={100500}
staticAssets={mockCoreSetup.http.staticAssets}
basePath={mockCoreSetup.http.basePath}
title="Some Title"
body={<div>Some Body</div>}
@ -45,7 +45,7 @@ describe('PromptPage', () => {
const body = renderToStaticMarkup(
<PromptPage
buildNumber={100500}
staticAssets={mockCoreSetup.http.staticAssets}
basePath={mockCoreSetup.http.basePath}
scriptPaths={['/some/script1.js', '/some/script2.js']}
title="Some Title"

View file

@ -19,6 +19,7 @@ import { renderToString } from 'react-dom/server';
import type { IBasePath } from '@kbn/core/server';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
import type { IStaticAssets } from '@kbn/core-http-server';
import { Fonts } from '@kbn/core-rendering-server-internal';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n-react';
@ -35,7 +36,7 @@ appendIconComponentCache({
const emotionCache = createCache({ key: 'eui', stylisPlugins: [euiStylisPrefixer] });
interface Props {
buildNumber: number;
staticAssets: IStaticAssets;
basePath: IBasePath;
scriptPaths?: string[];
title: ReactNode;
@ -46,7 +47,7 @@ interface Props {
export function PromptPage({
basePath,
buildNumber,
staticAssets,
scriptPaths = [],
title,
body,
@ -74,8 +75,8 @@ export function PromptPage({
const chunks = extractCriticalToChunks(renderToString(content));
const emotionStyles = constructStyleTagsFromChunks(chunks);
const uiPublicURL = `${basePath.serverBasePath}/ui`;
const regularBundlePath = `${basePath.serverBasePath}/${buildNumber}/bundles`;
const uiPublicURL = staticAssets.prependPublicUrl('/ui');
const regularBundlePath = staticAssets.prependPublicUrl('/bundles');
const styleSheetPaths = [
`${regularBundlePath}/kbn-ui-shared-deps-src/${UiSharedDepsSrc.cssDistFilename}`,
`${regularBundlePath}/kbn-ui-shared-deps-npm/${UiSharedDepsNpm.lightCssDistFilename('v8')}`,