;
+ beforeEach(() => {
+ const [acsRouteConfig, acsRouteHandler] = router.post.mock.calls.find(
+ ([{ path }]) => path === '/internal/security/access_agreement/acknowledge'
+ )!;
+
+ license.getFeatures.mockReturnValue({
+ allowAccessAgreement: true,
+ } as SecurityLicenseFeatures);
+
+ routeConfig = acsRouteConfig;
+ routeHandler = acsRouteHandler;
+ });
+
+ it('correctly defines route.', () => {
+ expect(routeConfig.options).toBeUndefined();
+ expect(routeConfig.validate).toBe(false);
+ });
+
+ it(`returns 403 if current license doesn't allow access agreement acknowledgement.`, async () => {
+ license.getFeatures.mockReturnValue({
+ allowAccessAgreement: false,
+ } as SecurityLicenseFeatures);
+
+ const request = httpServerMock.createKibanaRequest();
+ await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({
+ status: 403,
+ payload: { message: `Current license doesn't support access agreement.` },
+ options: { body: { message: `Current license doesn't support access agreement.` } },
+ });
+ });
+
+ it('returns 500 if acknowledge throws unhandled exception.', async () => {
+ const unhandledException = new Error('Something went wrong.');
+ authc.acknowledgeAccessAgreement.mockRejectedValue(unhandledException);
+
+ const request = httpServerMock.createKibanaRequest();
+ await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({
+ status: 500,
+ payload: 'Internal Error',
+ options: {},
+ });
+ });
+
+ it('returns 204 if successfully acknowledged.', async () => {
+ authc.acknowledgeAccessAgreement.mockResolvedValue(undefined);
+
+ const request = httpServerMock.createKibanaRequest();
+ await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({
+ status: 204,
+ options: {},
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts
index abab67c9cd1d28..91783140539a5b 100644
--- a/x-pack/plugins/security/server/routes/authentication/common.ts
+++ b/x-pack/plugins/security/server/routes/authentication/common.ts
@@ -18,7 +18,13 @@ import { RouteDefinitionParams } from '..';
/**
* Defines routes that are common to various authentication mechanisms.
*/
-export function defineCommonRoutes({ router, authc, basePath, logger }: RouteDefinitionParams) {
+export function defineCommonRoutes({
+ router,
+ authc,
+ basePath,
+ license,
+ logger,
+}: RouteDefinitionParams) {
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
for (const path of ['/api/security/logout', '/api/security/v1/logout']) {
router.get(
@@ -135,4 +141,26 @@ export function defineCommonRoutes({ router, authc, basePath, logger }: RouteDef
}
})
);
+
+ router.post(
+ { path: '/internal/security/access_agreement/acknowledge', validate: false },
+ createLicensedRouteHandler(async (context, request, response) => {
+ // If license doesn't allow access agreement we shouldn't handle request.
+ if (!license.getFeatures().allowAccessAgreement) {
+ logger.warn(`Attempted to acknowledge access agreement when license doesn't allow it.`);
+ return response.forbidden({
+ body: { message: `Current license doesn't support access agreement.` },
+ });
+ }
+
+ try {
+ await authc.acknowledgeAccessAgreement(request);
+ } catch (err) {
+ logger.error(err);
+ return response.internalError();
+ }
+
+ return response.noContent();
+ })
+ );
}
diff --git a/x-pack/plugins/security/server/routes/licensed_route_handler.ts b/x-pack/plugins/security/server/routes/licensed_route_handler.ts
index b113b2ca59e3ef..d8c212aa2d2174 100644
--- a/x-pack/plugins/security/server/routes/licensed_route_handler.ts
+++ b/x-pack/plugins/security/server/routes/licensed_route_handler.ts
@@ -4,10 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { RequestHandler } from 'kibana/server';
+import { KibanaResponseFactory, RequestHandler, RouteMethod } from 'kibana/server';
-export const createLicensedRouteHandler = (handler: RequestHandler
) => {
- const licensedRouteHandler: RequestHandler
= (context, request, responseToolkit) => {
+export const createLicensedRouteHandler = <
+ P,
+ Q,
+ B,
+ M extends RouteMethod,
+ R extends KibanaResponseFactory
+>(
+ handler: RequestHandler
+) => {
+ const licensedRouteHandler: RequestHandler
= (
+ context,
+ request,
+ responseToolkit
+ ) => {
const { license } = context.licensing;
const licenseCheck = license.check('security', 'basic');
if (licenseCheck.state === 'unavailable' || licenseCheck.state === 'invalid') {
diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts
index fd05821f9d5206..c163ff4e256cd2 100644
--- a/x-pack/plugins/security/server/routes/users/change_password.test.ts
+++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts
@@ -53,7 +53,7 @@ describe('Change password', () => {
now: Date.now(),
idleTimeoutExpiration: null,
lifespanExpiration: null,
- provider: 'basic',
+ provider: { type: 'basic', name: 'basic' },
});
mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts
new file mode 100644
index 00000000000000..3d616575b84131
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts
@@ -0,0 +1,177 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ RequestHandler,
+ RouteConfig,
+ kibanaResponseFactory,
+ IRouter,
+ HttpResources,
+ HttpResourcesRequestHandler,
+ RequestHandlerContext,
+} from '../../../../../../src/core/server';
+import { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing';
+import { AuthenticationProvider } from '../../../common/types';
+import { ConfigType } from '../../config';
+import { defineAccessAgreementRoutes } from './access_agreement';
+
+import { httpResourcesMock, httpServerMock } from '../../../../../../src/core/server/mocks';
+import { routeDefinitionParamsMock } from '../index.mock';
+import { Authentication } from '../../authentication';
+
+describe('Access agreement view routes', () => {
+ let httpResources: jest.Mocked;
+ let router: jest.Mocked;
+ let config: ConfigType;
+ let authc: jest.Mocked;
+ let license: jest.Mocked;
+ let mockContext: RequestHandlerContext;
+ beforeEach(() => {
+ const routeParamsMock = routeDefinitionParamsMock.create();
+ router = routeParamsMock.router;
+ httpResources = routeParamsMock.httpResources;
+ authc = routeParamsMock.authc;
+ config = routeParamsMock.config;
+ license = routeParamsMock.license;
+
+ license.getFeatures.mockReturnValue({
+ allowAccessAgreement: true,
+ } as SecurityLicenseFeatures);
+
+ mockContext = ({
+ licensing: {
+ license: { check: jest.fn().mockReturnValue({ check: 'valid' }) },
+ },
+ } as unknown) as RequestHandlerContext;
+
+ defineAccessAgreementRoutes(routeParamsMock);
+ });
+
+ describe('View route', () => {
+ let routeHandler: HttpResourcesRequestHandler;
+ let routeConfig: RouteConfig;
+ beforeEach(() => {
+ const [viewRouteConfig, viewRouteHandler] = httpResources.register.mock.calls.find(
+ ([{ path }]) => path === '/security/access_agreement'
+ )!;
+
+ routeConfig = viewRouteConfig;
+ routeHandler = viewRouteHandler;
+ });
+
+ it('correctly defines route.', () => {
+ expect(routeConfig.options).toBeUndefined();
+ expect(routeConfig.validate).toBe(false);
+ });
+
+ it('does not render view if current license does not allow access agreement.', async () => {
+ const request = httpServerMock.createKibanaRequest();
+ const responseFactory = httpResourcesMock.createResponseFactory();
+
+ license.getFeatures.mockReturnValue({
+ allowAccessAgreement: false,
+ } as SecurityLicenseFeatures);
+
+ await routeHandler(mockContext, request, responseFactory);
+
+ expect(responseFactory.renderCoreApp).not.toHaveBeenCalledWith();
+ expect(responseFactory.forbidden).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders view.', async () => {
+ const request = httpServerMock.createKibanaRequest();
+ const responseFactory = httpResourcesMock.createResponseFactory();
+
+ await routeHandler(mockContext, request, responseFactory);
+
+ expect(responseFactory.renderCoreApp).toHaveBeenCalledWith();
+ });
+ });
+
+ describe('Access agreement state route', () => {
+ let routeHandler: RequestHandler;
+ let routeConfig: RouteConfig;
+ beforeEach(() => {
+ const [loginStateRouteConfig, loginStateRouteHandler] = router.get.mock.calls.find(
+ ([{ path }]) => path === '/internal/security/access_agreement/state'
+ )!;
+
+ routeConfig = loginStateRouteConfig;
+ routeHandler = loginStateRouteHandler;
+ });
+
+ it('correctly defines route.', () => {
+ expect(routeConfig.options).toBeUndefined();
+ expect(routeConfig.validate).toBe(false);
+ });
+
+ it('returns `403` if current license does not allow access agreement.', async () => {
+ const request = httpServerMock.createKibanaRequest();
+
+ license.getFeatures.mockReturnValue({
+ allowAccessAgreement: false,
+ } as SecurityLicenseFeatures);
+
+ await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({
+ status: 403,
+ payload: { message: `Current license doesn't support access agreement.` },
+ options: { body: { message: `Current license doesn't support access agreement.` } },
+ });
+ });
+
+ it('returns empty `accessAgreement` if session info is not available.', async () => {
+ const request = httpServerMock.createKibanaRequest();
+
+ authc.getSessionInfo.mockResolvedValue(null);
+
+ await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({
+ options: { body: { accessAgreement: '' } },
+ payload: { accessAgreement: '' },
+ status: 200,
+ });
+ });
+
+ it('returns non-empty `accessAgreement` only if it is configured.', async () => {
+ const request = httpServerMock.createKibanaRequest();
+
+ config.authc = routeDefinitionParamsMock.create({
+ authc: {
+ providers: {
+ basic: { basic1: { order: 0 } },
+ saml: {
+ saml1: {
+ order: 1,
+ realm: 'realm1',
+ accessAgreement: { message: 'Some access agreement' },
+ },
+ },
+ },
+ },
+ }).config.authc;
+
+ const cases: Array<[AuthenticationProvider, string]> = [
+ [{ type: 'basic', name: 'basic1' }, ''],
+ [{ type: 'saml', name: 'saml1' }, 'Some access agreement'],
+ [{ type: 'unknown-type', name: 'unknown-name' }, ''],
+ ];
+
+ for (const [sessionProvider, expectedAccessAgreement] of cases) {
+ authc.getSessionInfo.mockResolvedValue({
+ now: Date.now(),
+ idleTimeoutExpiration: null,
+ lifespanExpiration: null,
+ provider: sessionProvider,
+ });
+
+ await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({
+ options: { body: { accessAgreement: expectedAccessAgreement } },
+ payload: { accessAgreement: expectedAccessAgreement },
+ status: 200,
+ });
+ }
+ });
+ });
+});
diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts
new file mode 100644
index 00000000000000..49e1ff42a28a2a
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ConfigType } from '../../config';
+import { createLicensedRouteHandler } from '../licensed_route_handler';
+import { RouteDefinitionParams } from '..';
+
+/**
+ * Defines routes required for the Access Agreement view.
+ */
+export function defineAccessAgreementRoutes({
+ authc,
+ httpResources,
+ license,
+ config,
+ router,
+ logger,
+}: RouteDefinitionParams) {
+ // If license doesn't allow access agreement we shouldn't handle request.
+ const canHandleRequest = () => license.getFeatures().allowAccessAgreement;
+
+ httpResources.register(
+ { path: '/security/access_agreement', validate: false },
+ createLicensedRouteHandler(async (context, request, response) =>
+ canHandleRequest()
+ ? response.renderCoreApp()
+ : response.forbidden({
+ body: { message: `Current license doesn't support access agreement.` },
+ })
+ )
+ );
+
+ router.get(
+ { path: '/internal/security/access_agreement/state', validate: false },
+ createLicensedRouteHandler(async (context, request, response) => {
+ if (!canHandleRequest()) {
+ return response.forbidden({
+ body: { message: `Current license doesn't support access agreement.` },
+ });
+ }
+
+ // It's not guaranteed that we'll have session for the authenticated user (e.g. when user is
+ // authenticated with the help of HTTP authentication), that means we should safely check if
+ // we have it and can get a corresponding configuration.
+ try {
+ const session = await authc.getSessionInfo(request);
+ const accessAgreement =
+ (session &&
+ config.authc.providers[
+ session.provider.type as keyof ConfigType['authc']['providers']
+ ]?.[session.provider.name]?.accessAgreement?.message) ||
+ '';
+
+ return response.ok({ body: { accessAgreement } });
+ } catch (err) {
+ logger.error(err);
+ return response.internalError();
+ }
+ })
+ );
+}
diff --git a/x-pack/plugins/security/server/routes/views/index.test.ts b/x-pack/plugins/security/server/routes/views/index.test.ts
index a8e7e905b119af..7cddef9bf2b982 100644
--- a/x-pack/plugins/security/server/routes/views/index.test.ts
+++ b/x-pack/plugins/security/server/routes/views/index.test.ts
@@ -20,15 +20,18 @@ describe('View routes', () => {
expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path))
.toMatchInlineSnapshot(`
Array [
+ "/security/access_agreement",
"/security/account",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
]
`);
- expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(
- `Array []`
- );
+ expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
+ Array [
+ "/internal/security/access_agreement/state",
+ ]
+ `);
});
it('registers Login routes if `basic` provider is enabled', () => {
@@ -43,6 +46,7 @@ describe('View routes', () => {
.toMatchInlineSnapshot(`
Array [
"/login",
+ "/security/access_agreement",
"/security/account",
"/security/logged_out",
"/logout",
@@ -52,6 +56,7 @@ describe('View routes', () => {
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Array [
"/internal/security/login_state",
+ "/internal/security/access_agreement/state",
]
`);
});
@@ -68,6 +73,7 @@ describe('View routes', () => {
.toMatchInlineSnapshot(`
Array [
"/login",
+ "/security/access_agreement",
"/security/account",
"/security/logged_out",
"/logout",
@@ -77,6 +83,7 @@ describe('View routes', () => {
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Array [
"/internal/security/login_state",
+ "/internal/security/access_agreement/state",
]
`);
});
@@ -93,6 +100,7 @@ describe('View routes', () => {
.toMatchInlineSnapshot(`
Array [
"/login",
+ "/security/access_agreement",
"/security/account",
"/security/logged_out",
"/logout",
@@ -102,6 +110,7 @@ describe('View routes', () => {
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Array [
"/internal/security/login_state",
+ "/internal/security/access_agreement/state",
]
`);
});
diff --git a/x-pack/plugins/security/server/routes/views/index.ts b/x-pack/plugins/security/server/routes/views/index.ts
index 255989dfeb90cd..b9de58d47fe407 100644
--- a/x-pack/plugins/security/server/routes/views/index.ts
+++ b/x-pack/plugins/security/server/routes/views/index.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { defineAccessAgreementRoutes } from './access_agreement';
import { defineAccountManagementRoutes } from './account_management';
import { defineLoggedOutRoutes } from './logged_out';
import { defineLoginRoutes } from './login';
@@ -20,6 +21,7 @@ export function defineViewRoutes(params: RouteDefinitionParams) {
defineLoginRoutes(params);
}
+ defineAccessAgreementRoutes(params);
defineAccountManagementRoutes(params);
defineLoggedOutRoutes(params);
defineLogoutRoutes(params);
diff --git a/x-pack/plugins/security/server/routes/views/logged_out.test.ts b/x-pack/plugins/security/server/routes/views/logged_out.test.ts
index 3ff05d242d9dde..7cb73c49f9cbc8 100644
--- a/x-pack/plugins/security/server/routes/views/logged_out.test.ts
+++ b/x-pack/plugins/security/server/routes/views/logged_out.test.ts
@@ -39,7 +39,7 @@ describe('LoggedOut view routes', () => {
it('redirects user to the root page if they have a session already.', async () => {
authc.getSessionInfo.mockResolvedValue({
- provider: 'basic',
+ provider: { type: 'basic', name: 'basic' },
now: 0,
idleTimeoutExpiration: null,
lifespanExpiration: null,
diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts
index d43319efbdfb9f..014ad390a3d53b 100644
--- a/x-pack/plugins/security/server/routes/views/login.test.ts
+++ b/x-pack/plugins/security/server/routes/views/login.test.ts
@@ -15,7 +15,7 @@ import {
RouteConfig,
} from '../../../../../../src/core/server';
import { SecurityLicense } from '../../../common/licensing';
-import { LoginState } from '../../../common/login_state';
+import { LoginSelectorProvider } from '../../../common/login_state';
import { ConfigType } from '../../config';
import { defineLoginRoutes } from './login';
@@ -141,6 +141,10 @@ describe('Login view routes', () => {
});
describe('Login state route', () => {
+ function getAuthcConfig(authcConfig: Record = {}) {
+ return routeDefinitionParamsMock.create({ authc: { ...authcConfig } }).config.authc;
+ }
+
let routeHandler: RequestHandler;
let routeConfig: RouteConfig;
beforeEach(() => {
@@ -159,6 +163,7 @@ describe('Login view routes', () => {
it('returns only required license features.', async () => {
license.getFeatures.mockReturnValue({
+ allowAccessAgreement: true,
allowLogin: true,
allowRbac: false,
allowRoleDocumentLevelSecurity: true,
@@ -176,9 +181,11 @@ describe('Login view routes', () => {
const expectedPayload = {
allowLogin: true,
layout: 'error-es-unavailable',
- showLoginForm: true,
requiresSecureConnection: false,
- selector: { enabled: false, providers: [] },
+ selector: {
+ enabled: false,
+ providers: [{ name: 'basic', type: 'basic', usesLoginForm: true }],
+ },
};
await expect(
routeHandler({ core: contextMock } as any, request, kibanaResponseFactory)
@@ -198,9 +205,11 @@ describe('Login view routes', () => {
const expectedPayload = {
allowLogin: true,
layout: 'form',
- showLoginForm: true,
requiresSecureConnection: false,
- selector: { enabled: false, providers: [] },
+ selector: {
+ enabled: false,
+ providers: [{ name: 'basic', type: 'basic', usesLoginForm: true }],
+ },
};
await expect(
routeHandler({ core: contextMock } as any, request, kibanaResponseFactory)
@@ -229,22 +238,46 @@ describe('Login view routes', () => {
});
});
- it('returns `showLoginForm: true` only if either `basic` or `token` provider is enabled.', async () => {
+ it('returns `useLoginForm: true` for `basic` and `token` providers.', async () => {
license.getFeatures.mockReturnValue({ allowLogin: true, showLogin: true } as any);
const request = httpServerMock.createKibanaRequest();
const contextMock = coreMock.createRequestHandlerContext();
- const cases: Array<[boolean, ConfigType['authc']['sortedProviders']]> = [
- [false, []],
- [true, [{ type: 'basic', name: 'basic1', options: { order: 0, showInSelector: true } }]],
- [true, [{ type: 'token', name: 'token1', options: { order: 0, showInSelector: true } }]],
+ const cases: Array<[LoginSelectorProvider[], ConfigType['authc']]> = [
+ [[], getAuthcConfig({ providers: { basic: { basic1: { order: 0, enabled: false } } } })],
+ [
+ [
+ {
+ name: 'basic1',
+ type: 'basic',
+ usesLoginForm: true,
+ icon: 'logoElastic',
+ description: 'Log in with Elasticsearch',
+ },
+ ],
+ getAuthcConfig({ providers: { basic: { basic1: { order: 0 } } } }),
+ ],
+ [
+ [
+ {
+ name: 'token1',
+ type: 'token',
+ usesLoginForm: true,
+ icon: 'logoElastic',
+ description: 'Log in with Elasticsearch',
+ },
+ ],
+ getAuthcConfig({ providers: { token: { token1: { order: 0 } } } }),
+ ],
];
- for (const [showLoginForm, sortedProviders] of cases) {
- config.authc.sortedProviders = sortedProviders;
+ for (const [providers, authcConfig] of cases) {
+ config.authc = authcConfig;
- const expectedPayload = expect.objectContaining({ showLoginForm });
+ const expectedPayload = expect.objectContaining({
+ selector: { enabled: false, providers },
+ });
await expect(
routeHandler({ core: contextMock } as any, request, kibanaResponseFactory)
).resolves.toEqual({
@@ -261,81 +294,142 @@ describe('Login view routes', () => {
const request = httpServerMock.createKibanaRequest();
const contextMock = coreMock.createRequestHandlerContext();
- const cases: Array<[
- boolean,
- ConfigType['authc']['sortedProviders'],
- LoginState['selector']['providers']
- ]> = [
- // selector is disabled, providers shouldn't be returned.
+ const cases: Array<[ConfigType['authc'], LoginSelectorProvider[]]> = [
+ // selector is disabled, multiple providers, but only basic provider should be returned.
[
- false,
+ getAuthcConfig({
+ selector: { enabled: false },
+ providers: {
+ basic: { basic1: { order: 0 } },
+ saml: { saml1: { order: 1, realm: 'realm1' } },
+ },
+ }),
[
- { type: 'basic', name: 'basic1', options: { order: 0, showInSelector: true } },
- { type: 'saml', name: 'saml1', options: { order: 1, showInSelector: true } },
+ {
+ name: 'basic1',
+ type: 'basic',
+ usesLoginForm: true,
+ icon: 'logoElastic',
+ description: 'Log in with Elasticsearch',
+ },
],
- [],
],
- // selector is enabled, but only basic/token is available, providers shouldn't be returned.
+ // selector is enabled, but only basic/token is available and should be returned.
[
- true,
- [{ type: 'basic', name: 'basic1', options: { order: 0, showInSelector: true } }],
- [],
+ getAuthcConfig({
+ selector: { enabled: true },
+ providers: { basic: { basic1: { order: 0 } } },
+ }),
+ [
+ {
+ name: 'basic1',
+ type: 'basic',
+ usesLoginForm: true,
+ icon: 'logoElastic',
+ description: 'Log in with Elasticsearch',
+ },
+ ],
],
- // selector is enabled, non-basic/token providers should be returned
+ // selector is enabled, all providers should be returned
[
- true,
+ getAuthcConfig({
+ selector: { enabled: true },
+ providers: {
+ basic: {
+ basic1: {
+ order: 0,
+ description: 'some-desc1',
+ hint: 'some-hint1',
+ icon: 'logoElastic',
+ },
+ },
+ saml: {
+ saml1: { order: 1, description: 'some-desc2', realm: 'realm1', icon: 'some-icon2' },
+ saml2: { order: 2, description: 'some-desc3', hint: 'some-hint3', realm: 'realm2' },
+ },
+ },
+ }),
[
{
type: 'basic',
name: 'basic1',
- options: { order: 0, showInSelector: true, description: 'some-desc1' },
+ description: 'some-desc1',
+ hint: 'some-hint1',
+ icon: 'logoElastic',
+ usesLoginForm: true,
},
{
type: 'saml',
name: 'saml1',
- options: { order: 1, showInSelector: true, description: 'some-desc2' },
+ description: 'some-desc2',
+ icon: 'some-icon2',
+ usesLoginForm: false,
},
{
type: 'saml',
name: 'saml2',
- options: { order: 2, showInSelector: true, description: 'some-desc3' },
+ description: 'some-desc3',
+ hint: 'some-hint3',
+ usesLoginForm: false,
},
],
- [
- { type: 'saml', name: 'saml1', description: 'some-desc2' },
- { type: 'saml', name: 'saml2', description: 'some-desc3' },
- ],
],
- // selector is enabled, only non-basic/token providers that are enabled in selector should be returned.
+ // selector is enabled, only providers that are enabled should be returned.
[
- true,
+ getAuthcConfig({
+ selector: { enabled: true },
+ providers: {
+ basic: {
+ basic1: {
+ order: 0,
+ description: 'some-desc1',
+ hint: 'some-hint1',
+ icon: 'some-icon1',
+ },
+ },
+ saml: {
+ saml1: {
+ order: 1,
+ description: 'some-desc2',
+ realm: 'realm1',
+ showInSelector: false,
+ },
+ saml2: {
+ order: 2,
+ description: 'some-desc3',
+ hint: 'some-hint3',
+ icon: 'some-icon3',
+ realm: 'realm2',
+ },
+ },
+ },
+ }),
[
{
type: 'basic',
name: 'basic1',
- options: { order: 0, showInSelector: true, description: 'some-desc1' },
- },
- {
- type: 'saml',
- name: 'saml1',
- options: { order: 1, showInSelector: false, description: 'some-desc2' },
+ description: 'some-desc1',
+ hint: 'some-hint1',
+ icon: 'some-icon1',
+ usesLoginForm: true,
},
{
type: 'saml',
name: 'saml2',
- options: { order: 2, showInSelector: true, description: 'some-desc3' },
+ description: 'some-desc3',
+ hint: 'some-hint3',
+ icon: 'some-icon3',
+ usesLoginForm: false,
},
],
- [{ type: 'saml', name: 'saml2', description: 'some-desc3' }],
],
];
- for (const [selectorEnabled, sortedProviders, expectedProviders] of cases) {
- config.authc.selector.enabled = selectorEnabled;
- config.authc.sortedProviders = sortedProviders;
+ for (const [authcConfig, expectedProviders] of cases) {
+ config.authc = authcConfig;
const expectedPayload = expect.objectContaining({
- selector: { enabled: selectorEnabled, providers: expectedProviders },
+ selector: { enabled: authcConfig.selector.enabled, providers: expectedProviders },
});
await expect(
routeHandler({ core: contextMock } as any, request, kibanaResponseFactory)
diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts
index 4d6747de713f76..f72facb2e24cc9 100644
--- a/x-pack/plugins/security/server/routes/views/login.ts
+++ b/x-pack/plugins/security/server/routes/views/login.ts
@@ -55,15 +55,16 @@ export function defineLoginRoutes({
const { allowLogin, layout = 'form' } = license.getFeatures();
const { sortedProviders, selector } = config.authc;
- let showLoginForm = false;
const providers = [];
- for (const { type, name, options } of sortedProviders) {
- if (options.showInSelector) {
- if (type === 'basic' || type === 'token') {
- showLoginForm = true;
- } else if (selector.enabled) {
- providers.push({ type, name, description: options.description });
- }
+ for (const { type, name } of sortedProviders) {
+ // Since `config.authc.sortedProviders` is based on `config.authc.providers` config we can
+ // be sure that config is present for every provider in `config.authc.sortedProviders`.
+ const { showInSelector, description, hint, icon } = config.authc.providers[type]?.[name]!;
+
+ // Include provider into the list if either selector is enabled or provider uses login form.
+ const usesLoginForm = type === 'basic' || type === 'token';
+ if (showInSelector && (usesLoginForm || selector.enabled)) {
+ providers.push({ type, name, usesLoginForm, description, hint, icon });
}
}
@@ -71,7 +72,7 @@ export function defineLoginRoutes({
allowLogin,
layout,
requiresSecureConnection: config.secureCookies,
- showLoginForm,
+ loginHelp: config.loginHelp,
selector: { enabled: selector.enabled, providers },
};
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 5bb08915165170..cdff34ec3a6039 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -2579,7 +2579,6 @@
"telemetry.welcomeBanner.enableButtonLabel": "有効にする",
"telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "遠隔測定に関するプライバシーステートメント",
"telemetry.welcomeBanner.title": "Elastic Stack の改善にご協力ください",
- "tileMap.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子は data-update に対応できるようこのメソドを導入する必要があります",
"tileMap.function.help": "タイルマップのビジュアライゼーションです",
"tileMap.geohashLayer.mapTitle": "{mapType} マップタイプが認識されません",
"tileMap.tooltipFormatter.latitudeLabel": "緯度",
@@ -2601,25 +2600,6 @@
"tileMap.visParams.desaturateTilesLabel": "タイルを不飽和化",
"tileMap.visParams.mapTypeLabel": "マップタイプ",
"tileMap.visParams.reduceVibrancyOfTileColorsTip": "色の鮮明度を下げます。この機能は Internet Explorer ではバージョンにかかわらず利用できません。",
- "tileMap.wmsOptions.attributionStringTip": "右下角の属性文字列",
- "tileMap.wmsOptions.baseLayerSettingsTitle": "ベースレイヤー設定",
- "tileMap.wmsOptions.imageFormatToUseTip": "通常画像/png または画像/jpeg です。サーバーが透明レイヤーを返す場合は png を使用します。",
- "tileMap.wmsOptions.layersLabel": "レイヤー",
- "tileMap.wmsOptions.listOfLayersToUseTip": "使用するレイヤーのコンマ区切りのリストです。",
- "tileMap.wmsOptions.mapLoadFailDescription": "このパラメーターが正しくないと、マップが正常に読み込まれません。",
- "tileMap.wmsOptions.urlOfWMSWebServiceTip": "WMS web サービスの URL です。",
- "tileMap.wmsOptions.useWMSCompliantMapTileServerTip": "WMS 対応のマップタイルサーバーを使用します。上級者向けです。",
- "tileMap.wmsOptions.versionOfWMSserverSupportsTip": "サーバーがサポートしている WMS のバージョンです。",
- "tileMap.wmsOptions.wmsAttributionLabel": "WMS 属性",
- "tileMap.wmsOptions.wmsDescription": "WMS は、マップイメージサービスの {wmsLink} です。",
- "tileMap.wmsOptions.wmsFormatLabel": "WMS フォーマット",
- "tileMap.wmsOptions.wmsLayersLabel": "WMS レイヤー",
- "tileMap.wmsOptions.wmsLinkText": "OGC スタンダード",
- "tileMap.wmsOptions.wmsMapServerLabel": "WMS マップサーバー",
- "tileMap.wmsOptions.wmsServerSupportedStylesListTip": "WMS サーバーがサポートしている使用スタイルのコンマ区切りのリストです。大抵は空白のままです。",
- "tileMap.wmsOptions.wmsStylesLabel": "WMS スタイル",
- "tileMap.wmsOptions.wmsUrlLabel": "WMS URL",
- "tileMap.wmsOptions.wmsVersionLabel": "WMS バージョン",
"timelion.badge.readOnly.text": "読み込み専用",
"timelion.badge.readOnly.tooltip": "Timelion シートを保存できません",
"timelion.breadcrumbs.create": "作成",
@@ -8325,13 +8305,6 @@
"xpack.ingestManager.agentListStatus.offlineLabel": "オフライン",
"xpack.ingestManager.agentListStatus.onlineLabel": "オンライン",
"xpack.ingestManager.agentListStatus.totalLabel": "エージェント",
- "xpack.ingestManager.apiKeysForm.configLabel": "構成",
- "xpack.ingestManager.apiKeysForm.nameLabel": "キー名",
- "xpack.ingestManager.apiKeysForm.saveButton": "保存",
- "xpack.ingestManager.apiKeysList.apiKeyColumnTitle": "API キー",
- "xpack.ingestManager.apiKeysList.configColumnTitle": "構成",
- "xpack.ingestManager.apiKeysList.emptyEnrollmentKeysMessage": "API キーがありません",
- "xpack.ingestManager.apiKeysList.nameColumnTitle": "名前",
"xpack.ingestManager.appNavigation.configurationsLinkText": "構成",
"xpack.ingestManager.appNavigation.fleetLinkText": "フリート",
"xpack.ingestManager.appNavigation.overviewLinkText": "概要",
@@ -8410,8 +8383,6 @@
"xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle": "{count} 件のエージェント構成を削除しました",
"xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle": "エージェント構成「{id}」を削除しました",
"xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel": "キャンセル",
- "xpack.ingestManager.deleteApiKeys.confirmModal.confirmButtonLabel": "削除",
- "xpack.ingestManager.deleteApiKeys.confirmModal.title": "API キーを削除: {apiKeyId}",
"xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage": "{agentConfigName} が一部のエージェントで既に使用されていることをフリートが検出しました。",
"xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle": "このアクションは {agentsCount} {agentsCount, plural, one {# エージェント} other {# エージェント}}に影響します",
"xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel": "キャンセル",
@@ -8434,9 +8405,7 @@
"xpack.ingestManager.editConfig.successNotificationTitle": "エージェント構成「{name}」を更新しました",
"xpack.ingestManager.enrollmentApiKeyForm.namePlaceholder": "名前を選択",
"xpack.ingestManager.enrollmentApiKeyList.createNewButton": "新規キーを作成",
- "xpack.ingestManager.enrollmentApiKeyList.hideTableButton": "を非表示",
"xpack.ingestManager.enrollmentApiKeyList.useExistingsButton": "既存のキーを使用",
- "xpack.ingestManager.enrollmentApiKeyList.viewTableButton": "表示",
"xpack.ingestManager.epm.addDatasourceButtonText": "データソースを作成",
"xpack.ingestManager.epm.pageSubtitle": "人気のアプリやサービスのパッケージを参照する",
"xpack.ingestManager.epm.pageTitle": "Elastic Package Manager",
@@ -12651,7 +12620,6 @@
"xpack.security.loginPage.esUnavailableTitle": "Elasticsearch クラスターに接続できません",
"xpack.security.loginPage.loginProviderDescription": "{providerType}/{providerName} でログイン",
"xpack.security.loginPage.loginSelectorErrorMessage": "ログインを実行できませんでした。",
- "xpack.security.loginPage.loginSelectorOR": "OR",
"xpack.security.loginPage.noLoginMethodsAvailableMessage": "システム管理者にお問い合わせください。",
"xpack.security.loginPage.noLoginMethodsAvailableTitle": "ログインが無効です。",
"xpack.security.loginPage.requiresSecureConnectionMessage": "システム管理者にお問い合わせください。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index a4f47053647377..819112feb9f572 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -2580,7 +2580,6 @@
"telemetry.welcomeBanner.enableButtonLabel": "启用",
"telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "遥测隐私声明",
"telemetry.welcomeBanner.title": "帮助我们改进 Elastic Stack",
- "tileMap.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子函数应实现此方法以响应数据更新",
"tileMap.function.help": "磁贴地图可视化",
"tileMap.geohashLayer.mapTitle": "{mapType} 地图类型无法识别",
"tileMap.tooltipFormatter.latitudeLabel": "纬度",
@@ -2602,25 +2601,6 @@
"tileMap.visParams.desaturateTilesLabel": "降低平铺地图饱和度",
"tileMap.visParams.mapTypeLabel": "地图类型",
"tileMap.visParams.reduceVibrancyOfTileColorsTip": "降低平铺地图颜色的亮度。此设置在任何版本的 IE 浏览器中均不起作用。",
- "tileMap.wmsOptions.attributionStringTip": "右下角的属性字符串。",
- "tileMap.wmsOptions.baseLayerSettingsTitle": "基础图层设置",
- "tileMap.wmsOptions.imageFormatToUseTip": "通常为 image/png 或 image/jpeg。如果服务器返回透明图层,则使用 png。",
- "tileMap.wmsOptions.layersLabel": "图层",
- "tileMap.wmsOptions.listOfLayersToUseTip": "要使用的图层逗号分隔列表。",
- "tileMap.wmsOptions.mapLoadFailDescription": "如果此参数不正确,将无法加载地图。",
- "tileMap.wmsOptions.urlOfWMSWebServiceTip": "WMS Web 服务的 URL。",
- "tileMap.wmsOptions.useWMSCompliantMapTileServerTip": "使用符合 WMS 规范的平铺地图服务器。仅适用于高级用户。",
- "tileMap.wmsOptions.versionOfWMSserverSupportsTip": "服务器支持的 WMS 版本。",
- "tileMap.wmsOptions.wmsAttributionLabel": "WMS 属性",
- "tileMap.wmsOptions.wmsDescription": "WMS 是用于地图图像服务的 {wmsLink}。",
- "tileMap.wmsOptions.wmsFormatLabel": "WMS 格式",
- "tileMap.wmsOptions.wmsLayersLabel": "WMS 图层",
- "tileMap.wmsOptions.wmsLinkText": "OGC 标准",
- "tileMap.wmsOptions.wmsMapServerLabel": "WMS 地图服务器",
- "tileMap.wmsOptions.wmsServerSupportedStylesListTip": "要使用的以逗号分隔的 WMS 服务器支持的样式列表。在大部分情况下为空。",
- "tileMap.wmsOptions.wmsStylesLabel": "WMS 样式",
- "tileMap.wmsOptions.wmsUrlLabel": "WMS url",
- "tileMap.wmsOptions.wmsVersionLabel": "WMS 版本",
"timelion.badge.readOnly.text": "只读",
"timelion.badge.readOnly.tooltip": "无法保存 Timelion 工作表",
"timelion.breadcrumbs.create": "创建",
@@ -8328,13 +8308,6 @@
"xpack.ingestManager.agentListStatus.offlineLabel": "脱机",
"xpack.ingestManager.agentListStatus.onlineLabel": "联机",
"xpack.ingestManager.agentListStatus.totalLabel": "代理",
- "xpack.ingestManager.apiKeysForm.configLabel": "配置",
- "xpack.ingestManager.apiKeysForm.nameLabel": "密钥名称",
- "xpack.ingestManager.apiKeysForm.saveButton": "保存",
- "xpack.ingestManager.apiKeysList.apiKeyColumnTitle": "API 密钥",
- "xpack.ingestManager.apiKeysList.configColumnTitle": "配置",
- "xpack.ingestManager.apiKeysList.emptyEnrollmentKeysMessage": "无 API 密钥",
- "xpack.ingestManager.apiKeysList.nameColumnTitle": "名称",
"xpack.ingestManager.appNavigation.configurationsLinkText": "配置",
"xpack.ingestManager.appNavigation.fleetLinkText": "Fleet",
"xpack.ingestManager.appNavigation.overviewLinkText": "概览",
@@ -8413,8 +8386,6 @@
"xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle": "已删除 {count} 个代理配置",
"xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle": "已删除代理配置“{id}”",
"xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel": "取消",
- "xpack.ingestManager.deleteApiKeys.confirmModal.confirmButtonLabel": "删除",
- "xpack.ingestManager.deleteApiKeys.confirmModal.title": "删除 api 密钥:{apiKeyId}",
"xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage": "Fleet 已检测到 {agentConfigName} 已由您的部分代理使用。",
"xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle": "此操作将影响 {agentsCount} 个 {agentsCount, plural, one {代理} other {代理}}。",
"xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel": "取消",
@@ -8437,9 +8408,7 @@
"xpack.ingestManager.editConfig.successNotificationTitle": "代理配置“{name}”已更新",
"xpack.ingestManager.enrollmentApiKeyForm.namePlaceholder": "选择名称",
"xpack.ingestManager.enrollmentApiKeyList.createNewButton": "创建新密钥",
- "xpack.ingestManager.enrollmentApiKeyList.hideTableButton": "隐藏",
"xpack.ingestManager.enrollmentApiKeyList.useExistingsButton": "使用现有密钥",
- "xpack.ingestManager.enrollmentApiKeyList.viewTableButton": "查看",
"xpack.ingestManager.epm.addDatasourceButtonText": "创建数据源",
"xpack.ingestManager.epm.pageSubtitle": "浏览热门应用和服务的软件。",
"xpack.ingestManager.epm.pageTitle": "Elastic Package Manager",
@@ -12655,7 +12624,6 @@
"xpack.security.loginPage.esUnavailableTitle": "无法连接到 Elasticsearch 集群",
"xpack.security.loginPage.loginProviderDescription": "使用 {providerType}/{providerName} 登录",
"xpack.security.loginPage.loginSelectorErrorMessage": "无法执行登录。",
- "xpack.security.loginPage.loginSelectorOR": "或",
"xpack.security.loginPage.noLoginMethodsAvailableMessage": "请联系您的管理员。",
"xpack.security.loginPage.noLoginMethodsAvailableTitle": "登录已禁用。",
"xpack.security.loginPage.requiresSecureConnectionMessage": "请联系您的管理员。",
diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts
index bdca506cc73385..0cdf1ca05feac2 100644
--- a/x-pack/plugins/upgrade_assistant/server/plugin.ts
+++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts
@@ -25,6 +25,8 @@ import { registerClusterCheckupRoutes } from './routes/cluster_checkup';
import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging';
import { registerReindexIndicesRoutes, createReindexWorker } from './routes/reindex_indices';
import { registerTelemetryRoutes } from './routes/telemetry';
+import { telemetrySavedObjectType, reindexOperationSavedObjectType } from './saved_object_types';
+
import { RouteDependencies } from './types';
interface PluginsSetup {
@@ -57,11 +59,14 @@ export class UpgradeAssistantServerPlugin implements Plugin {
}
setup(
- { http, getStartServices, capabilities }: CoreSetup,
+ { http, getStartServices, capabilities, savedObjects }: CoreSetup,
{ usageCollection, cloud, licensing }: PluginsSetup
) {
this.licensing = licensing;
+ savedObjects.registerType(reindexOperationSavedObjectType);
+ savedObjects.registerType(telemetrySavedObjectType);
+
const router = http.createRouter();
const dependencies: RouteDependencies = {
@@ -85,8 +90,12 @@ export class UpgradeAssistantServerPlugin implements Plugin {
registerTelemetryRoutes(dependencies);
if (usageCollection) {
- getStartServices().then(([{ savedObjects, elasticsearch }]) => {
- registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, savedObjects });
+ getStartServices().then(([{ savedObjects: savedObjectsService, elasticsearch }]) => {
+ registerUpgradeAssistantUsageCollector({
+ elasticsearch,
+ usageCollection,
+ savedObjects: savedObjectsService,
+ });
});
}
}
diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts
new file mode 100644
index 00000000000000..dee0a74d8994bb
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { reindexOperationSavedObjectType } from './reindex_operation_saved_object_type';
+export { telemetrySavedObjectType } from './telemetry_saved_object_type';
diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts
new file mode 100644
index 00000000000000..ba661fbeceb267
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsType } from 'src/core/server';
+
+import { REINDEX_OP_TYPE } from '../../common/types';
+
+export const reindexOperationSavedObjectType: SavedObjectsType = {
+ name: REINDEX_OP_TYPE,
+ hidden: false,
+ namespaceType: 'agnostic',
+ mappings: {
+ properties: {
+ reindexTaskId: {
+ type: 'keyword',
+ },
+ indexName: {
+ type: 'keyword',
+ },
+ newIndexName: {
+ type: 'keyword',
+ },
+ status: {
+ type: 'integer',
+ },
+ locked: {
+ type: 'date',
+ },
+ lastCompletedStep: {
+ type: 'integer',
+ },
+ errorMessage: {
+ type: 'keyword',
+ },
+ reindexTaskPercComplete: {
+ type: 'float',
+ },
+ runningReindexCount: {
+ type: 'integer',
+ },
+ reindexOptions: {
+ properties: {
+ openAndClose: {
+ type: 'boolean',
+ },
+ queueSettings: {
+ properties: {
+ queuedAt: {
+ type: 'long',
+ },
+ startedAt: {
+ type: 'long',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts
new file mode 100644
index 00000000000000..b1321e634c0f1c
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsType } from 'src/core/server';
+
+import { UPGRADE_ASSISTANT_TYPE } from '../../common/types';
+
+export const telemetrySavedObjectType: SavedObjectsType = {
+ name: UPGRADE_ASSISTANT_TYPE,
+ hidden: false,
+ namespaceType: 'agnostic',
+ mappings: {
+ properties: {
+ ui_open: {
+ properties: {
+ overview: {
+ type: 'long',
+ null_value: 0,
+ },
+ cluster: {
+ type: 'long',
+ null_value: 0,
+ },
+ indices: {
+ type: 'long',
+ null_value: 0,
+ },
+ },
+ },
+ ui_reindex: {
+ properties: {
+ close: {
+ type: 'long',
+ null_value: 0,
+ },
+ open: {
+ type: 'long',
+ null_value: 0,
+ },
+ start: {
+ type: 'long',
+ null_value: 0,
+ },
+ stop: {
+ type: 'long',
+ null_value: 0,
+ },
+ },
+ },
+ features: {
+ properties: {
+ deprecation_logging: {
+ properties: {
+ enabled: {
+ type: 'boolean',
+ null_value: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
diff --git a/x-pack/test/api_integration/apis/infra/index.js b/x-pack/test/api_integration/apis/infra/index.js
index 8bb3475da6cc9d..28a317893f5b29 100644
--- a/x-pack/test/api_integration/apis/infra/index.js
+++ b/x-pack/test/api_integration/apis/infra/index.js
@@ -11,6 +11,7 @@ export default function({ loadTestFile }) {
loadTestFile(require.resolve('./log_entries'));
loadTestFile(require.resolve('./log_entry_highlights'));
loadTestFile(require.resolve('./logs_without_millis'));
+ loadTestFile(require.resolve('./log_sources'));
loadTestFile(require.resolve('./log_summary'));
loadTestFile(require.resolve('./metrics'));
loadTestFile(require.resolve('./sources'));
diff --git a/x-pack/test/api_integration/apis/infra/log_sources.ts b/x-pack/test/api_integration/apis/infra/log_sources.ts
new file mode 100644
index 00000000000000..73d59bcdcd9a49
--- /dev/null
+++ b/x-pack/test/api_integration/apis/infra/log_sources.ts
@@ -0,0 +1,179 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+import { beforeEach } from 'mocha';
+import {
+ getLogSourceConfigurationSuccessResponsePayloadRT,
+ patchLogSourceConfigurationSuccessResponsePayloadRT,
+} from '../../../../plugins/infra/common/http_api/log_sources';
+import { decodeOrThrow } from '../../../../plugins/infra/common/runtime_types';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function({ getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const logSourceConfiguration = getService('infraLogSourceConfiguration');
+
+ describe('log sources api', () => {
+ before(() => esArchiver.load('infra/metrics_and_logs'));
+ after(() => esArchiver.unload('infra/metrics_and_logs'));
+ beforeEach(() => esArchiver.load('empty_kibana'));
+ afterEach(() => esArchiver.unload('empty_kibana'));
+
+ describe('source configuration get method for non-existant source', () => {
+ it('returns the default source configuration', async () => {
+ const response = await logSourceConfiguration
+ .createGetLogSourceConfigurationAgent('default')
+ .expect(200);
+
+ const {
+ data: { configuration, origin },
+ } = decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
+
+ expect(origin).to.be('fallback');
+ expect(configuration.name).to.be('Default');
+ expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*');
+ expect(configuration.fields.timestamp).to.be('@timestamp');
+ expect(configuration.fields.tiebreaker).to.be('_doc');
+ expect(configuration.logColumns[0]).to.have.key('timestampColumn');
+ expect(configuration.logColumns[1]).to.have.key('fieldColumn');
+ expect(configuration.logColumns[2]).to.have.key('messageColumn');
+ });
+ });
+
+ describe('source configuration patch method for non-existant source', () => {
+ it('creates a source configuration', async () => {
+ const response = await logSourceConfiguration
+ .createUpdateLogSourceConfigurationAgent('default', {
+ name: 'NAME',
+ description: 'DESCRIPTION',
+ logAlias: 'filebeat-**',
+ fields: {
+ tiebreaker: 'TIEBREAKER',
+ timestamp: 'TIMESTAMP',
+ },
+ logColumns: [
+ {
+ messageColumn: {
+ id: 'MESSAGE_COLUMN',
+ },
+ },
+ ],
+ })
+ .expect(200);
+
+ // check direct response
+ const {
+ data: { configuration, origin },
+ } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
+
+ expect(configuration.name).to.be('NAME');
+ expect(origin).to.be('stored');
+ expect(configuration.logAlias).to.be('filebeat-**');
+ expect(configuration.fields.timestamp).to.be('TIMESTAMP');
+ expect(configuration.fields.tiebreaker).to.be('TIEBREAKER');
+ expect(configuration.logColumns).to.have.length(1);
+ expect(configuration.logColumns[0]).to.have.key('messageColumn');
+
+ // check for persistence
+ const {
+ data: { configuration: persistedConfiguration },
+ } = await logSourceConfiguration.getLogSourceConfiguration('default');
+
+ expect(configuration).to.eql(persistedConfiguration);
+ });
+
+ it('creates a source configuration with default values for unspecified properties', async () => {
+ const response = await logSourceConfiguration
+ .createUpdateLogSourceConfigurationAgent('default', {})
+ .expect(200);
+
+ const {
+ data: { configuration, origin },
+ } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
+
+ expect(configuration.name).to.be('Default');
+ expect(origin).to.be('stored');
+ expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*');
+ expect(configuration.fields.timestamp).to.be('@timestamp');
+ expect(configuration.fields.tiebreaker).to.be('_doc');
+ expect(configuration.logColumns).to.have.length(3);
+ expect(configuration.logColumns[0]).to.have.key('timestampColumn');
+ expect(configuration.logColumns[1]).to.have.key('fieldColumn');
+ expect(configuration.logColumns[2]).to.have.key('messageColumn');
+
+ // check for persistence
+ const {
+ data: { configuration: persistedConfiguration, origin: persistedOrigin },
+ } = await logSourceConfiguration.getLogSourceConfiguration('default');
+
+ expect(persistedOrigin).to.be('stored');
+ expect(configuration).to.eql(persistedConfiguration);
+ });
+ });
+
+ describe('source configuration patch method for existing source', () => {
+ beforeEach(async () => {
+ await logSourceConfiguration.updateLogSourceConfiguration('default', {});
+ });
+
+ it('updates a source configuration', async () => {
+ const response = await logSourceConfiguration
+ .createUpdateLogSourceConfigurationAgent('default', {
+ name: 'NAME',
+ description: 'DESCRIPTION',
+ logAlias: 'filebeat-**',
+ fields: {
+ tiebreaker: 'TIEBREAKER',
+ timestamp: 'TIMESTAMP',
+ },
+ logColumns: [
+ {
+ messageColumn: {
+ id: 'MESSAGE_COLUMN',
+ },
+ },
+ ],
+ })
+ .expect(200);
+
+ const {
+ data: { configuration, origin },
+ } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
+
+ expect(configuration.name).to.be('NAME');
+ expect(origin).to.be('stored');
+ expect(configuration.logAlias).to.be('filebeat-**');
+ expect(configuration.fields.timestamp).to.be('TIMESTAMP');
+ expect(configuration.fields.tiebreaker).to.be('TIEBREAKER');
+ expect(configuration.logColumns).to.have.length(1);
+ expect(configuration.logColumns[0]).to.have.key('messageColumn');
+ });
+
+ it('partially updates a source configuration', async () => {
+ const response = await logSourceConfiguration
+ .createUpdateLogSourceConfigurationAgent('default', {
+ name: 'NAME',
+ })
+ .expect(200);
+
+ const {
+ data: { configuration, origin },
+ } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
+
+ expect(configuration.name).to.be('NAME');
+ expect(origin).to.be('stored');
+ expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*');
+ expect(configuration.fields.timestamp).to.be('@timestamp');
+ expect(configuration.fields.tiebreaker).to.be('_doc');
+ expect(configuration.logColumns).to.have.length(3);
+ expect(configuration.logColumns[0]).to.have.key('timestampColumn');
+ expect(configuration.logColumns[1]).to.have.key('fieldColumn');
+ expect(configuration.logColumns[2]).to.have.key('messageColumn');
+ });
+ });
+ });
+}
diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts
index ef7e48388ff660..fcdf268ff27b0a 100644
--- a/x-pack/test/api_integration/apis/security/session.ts
+++ b/x-pack/test/api_integration/apis/security/session.ts
@@ -56,7 +56,7 @@ export default function({ getService }: FtrProviderContext) {
expect(body.now).to.be.a('number');
expect(body.idleTimeoutExpiration).to.be.a('number');
expect(body.lifespanExpiration).to.be(null);
- expect(body.provider).to.be('basic');
+ expect(body.provider).to.eql({ type: 'basic', name: 'basic' });
});
it('should not extend the session', async () => {
diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts
index 84b8476bd1dd10..6dcc9bb291b02a 100644
--- a/x-pack/test/api_integration/services/index.ts
+++ b/x-pack/test/api_integration/services/index.ts
@@ -21,6 +21,7 @@ import {
} from './infraops_graphql_client';
import { SiemGraphQLClientProvider, SiemGraphQLClientFactoryProvider } from './siem_graphql_client';
import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration';
+import { InfraLogSourceConfigurationProvider } from './infra_log_source_configuration';
import { MachineLearningProvider } from './ml';
import { IngestManagerProvider } from './ingest_manager';
@@ -35,6 +36,7 @@ export const services = {
infraOpsGraphQLClient: InfraOpsGraphQLClientProvider,
infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider,
infraOpsSourceConfiguration: InfraOpsSourceConfigurationProvider,
+ infraLogSourceConfiguration: InfraLogSourceConfigurationProvider,
siemGraphQLClient: SiemGraphQLClientProvider,
siemGraphQLClientFactory: SiemGraphQLClientFactoryProvider,
supertestWithoutAuth: SupertestWithoutAuthProvider,
diff --git a/x-pack/test/api_integration/services/infra_log_source_configuration.ts b/x-pack/test/api_integration/services/infra_log_source_configuration.ts
new file mode 100644
index 00000000000000..851720895c6201
--- /dev/null
+++ b/x-pack/test/api_integration/services/infra_log_source_configuration.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ getLogSourceConfigurationPath,
+ getLogSourceConfigurationSuccessResponsePayloadRT,
+ PatchLogSourceConfigurationRequestBody,
+ patchLogSourceConfigurationRequestBodyRT,
+ patchLogSourceConfigurationResponsePayloadRT,
+} from '../../../plugins/infra/common/http_api/log_sources';
+import { decodeOrThrow } from '../../../plugins/infra/common/runtime_types';
+import { FtrProviderContext } from '../ftr_provider_context';
+
+export function InfraLogSourceConfigurationProvider({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const log = getService('log');
+
+ const createGetLogSourceConfigurationAgent = (sourceId: string) =>
+ supertest
+ .get(getLogSourceConfigurationPath(sourceId))
+ .set({
+ 'kbn-xsrf': 'some-xsrf-token',
+ })
+ .send();
+
+ const getLogSourceConfiguration = async (sourceId: string) => {
+ log.debug(`Fetching Logs UI source configuration "${sourceId}"`);
+
+ const response = await createGetLogSourceConfigurationAgent(sourceId);
+
+ return decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
+ };
+
+ const createUpdateLogSourceConfigurationAgent = (
+ sourceId: string,
+ sourceProperties: PatchLogSourceConfigurationRequestBody['data']
+ ) =>
+ supertest
+ .patch(getLogSourceConfigurationPath(sourceId))
+ .set({
+ 'kbn-xsrf': 'some-xsrf-token',
+ })
+ .send(patchLogSourceConfigurationRequestBodyRT.encode({ data: sourceProperties }));
+
+ const updateLogSourceConfiguration = async (
+ sourceId: string,
+ sourceProperties: PatchLogSourceConfigurationRequestBody['data']
+ ) => {
+ log.debug(
+ `Updating Logs UI source configuration "${sourceId}" with properties ${JSON.stringify(
+ sourceProperties
+ )}`
+ );
+
+ const response = await createUpdateLogSourceConfigurationAgent(sourceId, sourceProperties);
+
+ return decodeOrThrow(patchLogSourceConfigurationResponsePayloadRT)(response.body);
+ };
+
+ return {
+ createGetLogSourceConfigurationAgent,
+ createUpdateLogSourceConfigurationAgent,
+ getLogSourceConfiguration,
+ updateLogSourceConfiguration,
+ };
+}
diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts
index 4e5ebab90880e0..89ed51f65b9306 100644
--- a/x-pack/test/functional/apps/infra/link_to.ts
+++ b/x-pack/test/functional/apps/infra/link_to.ts
@@ -5,6 +5,7 @@
*/
import expect from '@kbn/expect';
+import { URL } from 'url';
import { FtrProviderContext } from '../../ftr_provider_context';
const ONE_HOUR = 60 * 60 * 1000;
@@ -28,8 +29,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
search: `time=${timestamp}&filter=trace.id:${traceId}`,
state: undefined,
};
- const expectedSearchString = `logFilter=(expression:'trace.id:${traceId}',kind:kuery)&logPosition=(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)&sourceId=default`;
- const expectedRedirectPath = '/logs/stream?';
await pageObjects.common.navigateToUrlWithBrowserHistory(
'infraLogs',
@@ -41,9 +40,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
);
await retry.tryForTime(5000, async () => {
const currentUrl = await browser.getCurrentUrl();
- const decodedUrl = decodeURIComponent(currentUrl);
- expect(decodedUrl).to.contain(expectedRedirectPath);
- expect(decodedUrl).to.contain(expectedSearchString);
+ const parsedUrl = new URL(currentUrl);
+
+ expect(parsedUrl.pathname).to.be('/app/logs/stream');
+ expect(parsedUrl.searchParams.get('logFilter')).to.be(
+ `(expression:'trace.id:${traceId}',kind:kuery)`
+ );
+ expect(parsedUrl.searchParams.get('logPosition')).to.be(
+ `(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)`
+ );
+ expect(parsedUrl.searchParams.get('sourceId')).to.be('default');
});
});
});
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
index 1b75f4a27766a7..d0ce18bbc1c54c 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
@@ -504,6 +504,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
}
);
+ // await first run to complete so we have an initial state
+ await retry.try(async () => {
+ const { alertInstances } = await alerting.alerts.getAlertState(alert.id);
+ expect(Object.keys(alertInstances).length).to.eql(instances.length);
+ });
+
// refresh to see alert
await browser.refresh();
@@ -514,12 +520,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
// click on first alert
await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name);
-
- // await first run to complete so we have an initial state
- await retry.try(async () => {
- const { alertInstances } = await alerting.alerts.getAlertState(alert.id);
- expect(Object.keys(alertInstances).length).to.eql(instances.length);
- });
});
const PAGE_SIZE = 10;
diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts
index d664357c3ba126..e5b840b335846f 100644
--- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts
+++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts
@@ -7,7 +7,7 @@
import { merge, omit, times, chunk, isEmpty } from 'lodash';
import uuid from 'uuid';
import expect from '@kbn/expect/expect.js';
-import moment, { Moment } from 'moment';
+import moment from 'moment';
import { FtrProviderContext } from '../../ftr_provider_context';
import { IEvent } from '../../../../plugins/event_log/server';
import { IValidatedEvent } from '../../../../plugins/event_log/server/types';
@@ -43,10 +43,8 @@ export default function({ getService }: FtrProviderContext) {
it('should support pagination for events', async () => {
const id = uuid.v4();
- const timestamp = moment();
- const [firstExpectedEvent, ...expectedEvents] = times(6, () =>
- fakeEvent(id, fakeEventTiming(timestamp.add(1, 's')))
- );
+ const [firstExpectedEvent, ...expectedEvents] = times(6, () => fakeEvent(id));
+
// run one first to create the SO and avoid clashes
await logTestEvent(id, firstExpectedEvent);
await Promise.all(expectedEvents.map(event => logTestEvent(id, event)));
@@ -82,10 +80,7 @@ export default function({ getService }: FtrProviderContext) {
it('should support sorting by event end', async () => {
const id = uuid.v4();
- const timestamp = moment();
- const [firstExpectedEvent, ...expectedEvents] = times(6, () =>
- fakeEvent(id, fakeEventTiming(timestamp.add(1, 's')))
- );
+ const [firstExpectedEvent, ...expectedEvents] = times(6, () => fakeEvent(id));
// run one first to create the SO and avoid clashes
await logTestEvent(id, firstExpectedEvent);
await Promise.all(expectedEvents.map(event => logTestEvent(id, event)));
@@ -106,21 +101,24 @@ export default function({ getService }: FtrProviderContext) {
it('should support date ranges for events', async () => {
const id = uuid.v4();
- const timestamp = moment();
-
- const firstEvent = fakeEvent(id, fakeEventTiming(timestamp));
+ // write a document that shouldn't be found in the inclusive date range search
+ const firstEvent = fakeEvent(id);
await logTestEvent(id, firstEvent);
- await delay(100);
- const start = timestamp.add(1, 's').toISOString();
+ // wait a second, get the start time for the date range search
+ await delay(1000);
+ const start = new Date().toISOString();
- const expectedEvents = times(6, () => fakeEvent(id, fakeEventTiming(timestamp.add(1, 's'))));
+ // write the documents that we should be found in the date range searches
+ const expectedEvents = times(6, () => fakeEvent(id));
await Promise.all(expectedEvents.map(event => logTestEvent(id, event)));
- const end = timestamp.add(1, 's').toISOString();
+ // get the end time for the date range search
+ const end = new Date().toISOString();
- await delay(100);
- const lastEvent = fakeEvent(id, fakeEventTiming(timestamp.add(1, 's')));
+ // write a document that shouldn't be found in the inclusive date range search
+ await delay(1000);
+ const lastEvent = fakeEvent(id);
await logTestEvent(id, lastEvent);
await retry.try(async () => {
@@ -195,29 +193,12 @@ export default function({ getService }: FtrProviderContext) {
.expect(200);
}
- function fakeEventTiming(start: Moment): Partial {
- return {
- event: {
- start: start.toISOString(),
- end: start
- .clone()
- .add(500, 'milliseconds')
- .toISOString(),
- },
- };
- }
-
function fakeEvent(id: string, overrides: Partial = {}): IEvent {
- const start = moment().toISOString();
- const end = moment().toISOString();
return merge(
{
event: {
provider: 'event_log_fixture',
action: 'test',
- start,
- end,
- duration: 1000000,
},
kibana: {
saved_objects: [
diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts
index 2de395308ce74c..31668e8345275e 100644
--- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts
+++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts
@@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import uuid from 'uuid';
import expect from '@kbn/expect/expect.js';
import { IEvent } from '../../../../plugins/event_log/server';
import { FtrProviderContext } from '../../ftr_provider_context';
@@ -97,7 +98,7 @@ export default function({ getService }: FtrProviderContext) {
await registerProviderActions('provider4', ['action1', 'action2']);
}
- const eventId = '1';
+ const eventId = uuid.v4();
const event: IEvent = {
event: { action: 'action1', provider: 'provider4' },
kibana: { saved_objects: [{ type: 'event_log_test', id: eventId }] },