+>(
+ handler: RequestHandler
+) => {
+ const licensedRouteHandler: RequestHandler
= (context, request, responseToolkit) => {
+ const { license } = context.licensing;
+ const licenseCheck = license.check('security', 'basic');
+ if (
+ licenseCheck.check === LICENSE_STATUS.Unavailable ||
+ licenseCheck.check === LICENSE_STATUS.Invalid
+ ) {
+ return responseToolkit.forbidden({ body: { message: licenseCheck.message! } });
+ }
+
+ return handler(context, request, responseToolkit);
+ };
+
+ return licensedRouteHandler;
+};
diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts
new file mode 100644
index 0000000000000..2bd7440d3ee70
--- /dev/null
+++ b/x-pack/plugins/security/server/saved_objects/index.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 { IClusterClient, KibanaRequest, LegacyRequest } from '../../../../../src/core/server';
+import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper';
+import { LegacyAPI } from '../plugin';
+import { Authorization } from '../authorization';
+import { SecurityAuditLogger } from '../audit';
+
+interface SetupSavedObjectsParams {
+ adminClusterClient: IClusterClient;
+ auditLogger: SecurityAuditLogger;
+ authz: Pick;
+ legacyAPI: Pick;
+}
+
+export function setupSavedObjects({
+ adminClusterClient,
+ auditLogger,
+ authz,
+ legacyAPI: { savedObjects },
+}: SetupSavedObjectsParams) {
+ const getKibanaRequest = (request: KibanaRequest | LegacyRequest) =>
+ request instanceof KibanaRequest ? request : KibanaRequest.from(request);
+ savedObjects.setScopedSavedObjectsClientFactory(({ request }) => {
+ const kibanaRequest = getKibanaRequest(request);
+ if (authz.mode.useRbacForRequest(kibanaRequest)) {
+ const internalRepository = savedObjects.getSavedObjectsRepository(
+ adminClusterClient.callAsInternalUser
+ );
+ return new savedObjects.SavedObjectsClient(internalRepository);
+ }
+
+ const callAsCurrentUserRepository = savedObjects.getSavedObjectsRepository(
+ adminClusterClient.asScoped(kibanaRequest).callAsCurrentUser
+ );
+ return new savedObjects.SavedObjectsClient(callAsCurrentUserRepository);
+ });
+
+ savedObjects.addScopedSavedObjectsClientWrapperFactory(
+ Number.MAX_SAFE_INTEGER - 1,
+ 'security',
+ ({ client, request }) => {
+ const kibanaRequest = getKibanaRequest(request);
+ if (authz.mode.useRbacForRequest(kibanaRequest)) {
+ return new SecureSavedObjectsClientWrapper({
+ actions: authz.actions,
+ auditLogger,
+ baseClient: client,
+ checkSavedObjectsPrivilegesAsCurrentUser: authz.checkSavedObjectsPrivilegesWithRequest(
+ kibanaRequest
+ ),
+ errors: savedObjects.SavedObjectsClient.errors,
+ });
+ }
+
+ return client;
+ }
+ );
+}
diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
new file mode 100644
index 0000000000000..f802c011f207e
--- /dev/null
+++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
@@ -0,0 +1,822 @@
+/*
+ * 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 { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper';
+import { Actions } from '../authorization';
+import { securityAuditLoggerMock } from '../audit/index.mock';
+import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
+import { SavedObjectsClientContract } from 'kibana/server';
+
+const createSecureSavedObjectsClientWrapperOptions = () => {
+ const actions = new Actions('some-version');
+ jest
+ .spyOn(actions.savedObject, 'get')
+ .mockImplementation((type: string, action: string) => `mock-saved_object:${type}/${action}`);
+
+ const forbiddenError = new Error('Mock ForbiddenError');
+ const generalError = new Error('Mock GeneralError');
+
+ const errors = ({
+ decorateForbiddenError: jest.fn().mockReturnValue(forbiddenError),
+ decorateGeneralError: jest.fn().mockReturnValue(generalError),
+ } as unknown) as jest.Mocked;
+
+ return {
+ actions,
+ baseClient: savedObjectsClientMock.create(),
+ checkSavedObjectsPrivilegesAsCurrentUser: jest.fn(),
+ errors,
+ auditLogger: securityAuditLoggerMock.create(),
+ forbiddenError,
+ generalError,
+ };
+};
+
+describe('#errors', () => {
+ test(`assigns errors from constructor to .errors`, () => {
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ expect(client.errors).toBe(options.errors);
+ });
+});
+
+describe(`spaces disabled`, () => {
+ describe('#create', () => {
+ test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => {
+ const type = 'foo';
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(
+ new Error('An actual error would happen here')
+ );
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ await expect(client.create(type)).rejects.toThrowError(options.generalError);
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'create')],
+ undefined
+ );
+ expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`throws decorated ForbiddenError when unauthorized`, async () => {
+ const type = 'foo';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: false,
+ username,
+ privileges: { [options.actions.savedObject.get(type, 'create')]: false },
+ });
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const attributes = { some_attr: 's' };
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.create(type, attributes, apiCallOptions)).rejects.toThrowError(
+ options.forbiddenError
+ );
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'create')],
+ apiCallOptions.namespace
+ );
+ expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
+ username,
+ 'create',
+ [type],
+ [options.actions.savedObject.get(type, 'create')],
+ { type, attributes, options: apiCallOptions }
+ );
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`returns result of baseClient.create when authorized`, async () => {
+ const type = 'foo';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: true,
+ username,
+ privileges: { [options.actions.savedObject.get(type, 'create')]: true },
+ });
+
+ const apiCallReturnValue = Symbol();
+ options.baseClient.create.mockReturnValue(apiCallReturnValue as any);
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const attributes = { some_attr: 's' };
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.create(type, attributes, apiCallOptions)).resolves.toBe(
+ apiCallReturnValue
+ );
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'create')],
+ apiCallOptions.namespace
+ );
+ expect(options.baseClient.create).toHaveBeenCalledWith(type, attributes, apiCallOptions);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
+ username,
+ 'create',
+ [type],
+ { type, attributes, options: apiCallOptions }
+ );
+ });
+ });
+
+ describe('#bulkCreate', () => {
+ test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
+ const type = 'foo';
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(
+ new Error('An actual error would happen here')
+ );
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(
+ client.bulkCreate([{ type, attributes: {} }], apiCallOptions)
+ ).rejects.toThrowError(options.generalError);
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'bulk_create')],
+ apiCallOptions.namespace
+ );
+ expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`throws decorated ForbiddenError when unauthorized`, async () => {
+ const type1 = 'foo';
+ const type2 = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: false,
+ username,
+ privileges: {
+ [options.actions.savedObject.get(type1, 'bulk_create')]: false,
+ [options.actions.savedObject.get(type2, 'bulk_create')]: true,
+ },
+ });
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const objects = [{ type: type1, attributes: {} }, { type: type2, attributes: {} }];
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.bulkCreate(objects, apiCallOptions)).rejects.toThrowError(
+ options.forbiddenError
+ );
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [
+ options.actions.savedObject.get(type1, 'bulk_create'),
+ options.actions.savedObject.get(type2, 'bulk_create'),
+ ],
+ apiCallOptions.namespace
+ );
+ expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
+ username,
+ 'bulk_create',
+ [type1, type2],
+ [options.actions.savedObject.get(type1, 'bulk_create')],
+ { objects, options: apiCallOptions }
+ );
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`returns result of baseClient.bulkCreate when authorized`, async () => {
+ const type1 = 'foo';
+ const type2 = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: true,
+ username,
+ privileges: {
+ [options.actions.savedObject.get(type1, 'bulk_create')]: true,
+ [options.actions.savedObject.get(type2, 'bulk_create')]: true,
+ },
+ });
+
+ const apiCallReturnValue = Symbol();
+ options.baseClient.bulkCreate.mockReturnValue(apiCallReturnValue as any);
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const objects = [
+ { type: type1, otherThing: 'sup', attributes: {} },
+ { type: type2, otherThing: 'everyone', attributes: {} },
+ ];
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.bulkCreate(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue);
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [
+ options.actions.savedObject.get(type1, 'bulk_create'),
+ options.actions.savedObject.get(type2, 'bulk_create'),
+ ],
+ apiCallOptions.namespace
+ );
+ expect(options.baseClient.bulkCreate).toHaveBeenCalledWith(objects, apiCallOptions);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
+ username,
+ 'bulk_create',
+ [type1, type2],
+ { objects, options: apiCallOptions }
+ );
+ });
+ });
+
+ describe('#delete', () => {
+ test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
+ const type = 'foo';
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(
+ new Error('An actual error would happen here')
+ );
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ await expect(client.delete(type, 'bar')).rejects.toThrowError(options.generalError);
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'delete')],
+ undefined
+ );
+ expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`throws decorated ForbiddenError when unauthorized`, async () => {
+ const type = 'foo';
+ const id = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: false,
+ username,
+ privileges: {
+ [options.actions.savedObject.get(type, 'delete')]: false,
+ },
+ });
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.delete(type, id, apiCallOptions)).rejects.toThrowError(
+ options.forbiddenError
+ );
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'delete')],
+ apiCallOptions.namespace
+ );
+ expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
+ username,
+ 'delete',
+ [type],
+ [options.actions.savedObject.get(type, 'delete')],
+ { type, id, options: apiCallOptions }
+ );
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`returns result of internalRepository.delete when authorized`, async () => {
+ const type = 'foo';
+ const id = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: true,
+ username,
+ privileges: { [options.actions.savedObject.get(type, 'delete')]: true },
+ });
+
+ const apiCallReturnValue = Symbol();
+ options.baseClient.delete.mockReturnValue(apiCallReturnValue as any);
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.delete(type, id, apiCallOptions)).resolves.toBe(apiCallReturnValue);
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'delete')],
+ apiCallOptions.namespace
+ );
+ expect(options.baseClient.delete).toHaveBeenCalledWith(type, id, apiCallOptions);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
+ username,
+ 'delete',
+ [type],
+ { type, id, options: apiCallOptions }
+ );
+ });
+ });
+
+ describe('#find', () => {
+ test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
+ const type = 'foo';
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(
+ new Error('An actual error would happen here')
+ );
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ await expect(client.find({ type })).rejects.toThrowError(options.generalError);
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'find')],
+ undefined
+ );
+ expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => {
+ const type = 'foo';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: false,
+ username,
+ privileges: { [options.actions.savedObject.get(type, 'find')]: false },
+ });
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const apiCallOptions = Object.freeze({ type, namespace: 'some-ns' });
+ await expect(client.find(apiCallOptions)).rejects.toThrowError(options.forbiddenError);
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'find')],
+ apiCallOptions.namespace
+ );
+ expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
+ username,
+ 'find',
+ [type],
+ [options.actions.savedObject.get(type, 'find')],
+ { options: apiCallOptions }
+ );
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => {
+ const type1 = 'foo';
+ const type2 = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: false,
+ username,
+ privileges: {
+ [options.actions.savedObject.get(type1, 'find')]: false,
+ [options.actions.savedObject.get(type2, 'find')]: true,
+ },
+ });
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const apiCallOptions = Object.freeze({ type: [type1, type2], namespace: 'some-ns' });
+ await expect(client.find(apiCallOptions)).rejects.toThrowError(options.forbiddenError);
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [
+ options.actions.savedObject.get(type1, 'find'),
+ options.actions.savedObject.get(type2, 'find'),
+ ],
+ apiCallOptions.namespace
+ );
+ expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
+ username,
+ 'find',
+ [type1, type2],
+ [options.actions.savedObject.get(type1, 'find')],
+ { options: apiCallOptions }
+ );
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`returns result of baseClient.find when authorized`, async () => {
+ const type = 'foo';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: true,
+ username,
+ privileges: { [options.actions.savedObject.get(type, 'find')]: true },
+ });
+
+ const apiCallReturnValue = Symbol();
+ options.baseClient.find.mockReturnValue(apiCallReturnValue as any);
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const apiCallOptions = Object.freeze({ type, namespace: 'some-ns' });
+ await expect(client.find(apiCallOptions)).resolves.toBe(apiCallReturnValue);
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'find')],
+ apiCallOptions.namespace
+ );
+ expect(options.baseClient.find).toHaveBeenCalledWith(apiCallOptions);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
+ username,
+ 'find',
+ [type],
+ { options: apiCallOptions }
+ );
+ });
+ });
+
+ describe('#bulkGet', () => {
+ test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
+ const type = 'foo';
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(
+ new Error('An actual error would happen here')
+ );
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ await expect(client.bulkGet([{ id: 'bar', type }])).rejects.toThrowError(
+ options.generalError
+ );
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'bulk_get')],
+ undefined
+ );
+ expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`throws decorated ForbiddenError when unauthorized`, async () => {
+ const type1 = 'foo';
+ const type2 = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: false,
+ username,
+ privileges: {
+ [options.actions.savedObject.get(type1, 'bulk_get')]: false,
+ [options.actions.savedObject.get(type2, 'bulk_get')]: true,
+ },
+ });
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const objects = [{ type: type1, id: `bar-${type1}` }, { type: type2, id: `bar-${type2}` }];
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.bulkGet(objects, apiCallOptions)).rejects.toThrowError(
+ options.forbiddenError
+ );
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [
+ options.actions.savedObject.get(type1, 'bulk_get'),
+ options.actions.savedObject.get(type2, 'bulk_get'),
+ ],
+ apiCallOptions.namespace
+ );
+ expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
+ username,
+ 'bulk_get',
+ [type1, type2],
+ [options.actions.savedObject.get(type1, 'bulk_get')],
+ { objects, options: apiCallOptions }
+ );
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`returns result of baseClient.bulkGet when authorized`, async () => {
+ const type1 = 'foo';
+ const type2 = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: true,
+ username,
+ privileges: {
+ [options.actions.savedObject.get(type1, 'bulk_get')]: true,
+ [options.actions.savedObject.get(type2, 'bulk_get')]: true,
+ },
+ });
+
+ const apiCallReturnValue = Symbol();
+ options.baseClient.bulkGet.mockReturnValue(apiCallReturnValue as any);
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const objects = [{ type: type1, id: `id-${type1}` }, { type: type2, id: `id-${type2}` }];
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.bulkGet(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue);
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [
+ options.actions.savedObject.get(type1, 'bulk_get'),
+ options.actions.savedObject.get(type2, 'bulk_get'),
+ ],
+ apiCallOptions.namespace
+ );
+ expect(options.baseClient.bulkGet).toHaveBeenCalledWith(objects, apiCallOptions);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
+ username,
+ 'bulk_get',
+ [type1, type2],
+ { objects, options: apiCallOptions }
+ );
+ });
+ });
+
+ describe('#get', () => {
+ test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
+ const type = 'foo';
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(
+ new Error('An actual error would happen here')
+ );
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ await expect(client.get(type, 'bar')).rejects.toThrowError(options.generalError);
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'get')],
+ undefined
+ );
+ expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`throws decorated ForbiddenError when unauthorized`, async () => {
+ const type = 'foo';
+ const id = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: false,
+ username,
+ privileges: {
+ [options.actions.savedObject.get(type, 'get')]: false,
+ },
+ });
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.get(type, id, apiCallOptions)).rejects.toThrowError(
+ options.forbiddenError
+ );
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'get')],
+ apiCallOptions.namespace
+ );
+ expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
+ username,
+ 'get',
+ [type],
+ [options.actions.savedObject.get(type, 'get')],
+ { type, id, options: apiCallOptions }
+ );
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`returns result of baseClient.get when authorized`, async () => {
+ const type = 'foo';
+ const id = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: true,
+ username,
+ privileges: { [options.actions.savedObject.get(type, 'get')]: true },
+ });
+
+ const apiCallReturnValue = Symbol();
+ options.baseClient.get.mockReturnValue(apiCallReturnValue as any);
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.get(type, id, apiCallOptions)).resolves.toBe(apiCallReturnValue);
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'get')],
+ apiCallOptions.namespace
+ );
+ expect(options.baseClient.get).toHaveBeenCalledWith(type, id, apiCallOptions);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
+ username,
+ 'get',
+ [type],
+ { type, id, options: apiCallOptions }
+ );
+ });
+ });
+
+ describe('#update', () => {
+ test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
+ const type = 'foo';
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(
+ new Error('An actual error would happen here')
+ );
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ await expect(client.update(type, 'bar', {})).rejects.toThrowError(options.generalError);
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'update')],
+ undefined
+ );
+ expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`throws decorated ForbiddenError when unauthorized`, async () => {
+ const type = 'foo';
+ const id = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: false,
+ username,
+ privileges: {
+ [options.actions.savedObject.get(type, 'update')]: false,
+ },
+ });
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const attributes = { some: 'attr' };
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.update(type, id, attributes, apiCallOptions)).rejects.toThrowError(
+ options.forbiddenError
+ );
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'update')],
+ apiCallOptions.namespace
+ );
+ expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
+ username,
+ 'update',
+ [type],
+ [options.actions.savedObject.get(type, 'update')],
+ { type, id, attributes, options: apiCallOptions }
+ );
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`returns result of baseClient.update when authorized`, async () => {
+ const type = 'foo';
+ const id = 'bar';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: true,
+ username,
+ privileges: { [options.actions.savedObject.get(type, 'update')]: true },
+ });
+
+ const apiCallReturnValue = Symbol();
+ options.baseClient.update.mockReturnValue(apiCallReturnValue as any);
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const attributes = { some: 'attr' };
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.update(type, id, attributes, apiCallOptions)).resolves.toBe(
+ apiCallReturnValue
+ );
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'update')],
+ apiCallOptions.namespace
+ );
+ expect(options.baseClient.update).toHaveBeenCalledWith(type, id, attributes, apiCallOptions);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
+ username,
+ 'update',
+ [type],
+ { type, id, attributes, options: apiCallOptions }
+ );
+ });
+ });
+
+ describe('#bulkUpdate', () => {
+ test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
+ const type = 'foo';
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(
+ new Error('An actual error would happen here')
+ );
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ await expect(client.bulkUpdate([{ id: 'bar', type, attributes: {} }])).rejects.toThrowError(
+ options.generalError
+ );
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'bulk_update')],
+ undefined
+ );
+ expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`throws decorated ForbiddenError when unauthorized`, async () => {
+ const type = 'foo';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: false,
+ username,
+ privileges: {
+ [options.actions.savedObject.get(type, 'bulk_update')]: false,
+ },
+ });
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const objects = [{ type, id: `bar-${type}`, attributes: {} }];
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.bulkUpdate(objects, apiCallOptions)).rejects.toThrowError(
+ options.forbiddenError
+ );
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'bulk_update')],
+ apiCallOptions.namespace
+ );
+ expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
+ username,
+ 'bulk_update',
+ [type],
+ [options.actions.savedObject.get(type, 'bulk_update')],
+ { objects, options: apiCallOptions }
+ );
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test(`returns result of baseClient.bulkUpdate when authorized`, async () => {
+ const type = 'foo';
+ const username = Symbol();
+ const options = createSecureSavedObjectsClientWrapperOptions();
+ options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({
+ hasAllRequested: true,
+ username,
+ privileges: {
+ [options.actions.savedObject.get(type, 'bulk_update')]: true,
+ },
+ });
+
+ const apiCallReturnValue = Symbol();
+ options.baseClient.bulkUpdate.mockReturnValue(apiCallReturnValue as any);
+
+ const client = new SecureSavedObjectsClientWrapper(options);
+
+ const objects = [{ type, id: `id-${type}`, attributes: {} }];
+ const apiCallOptions = Object.freeze({ namespace: 'some-ns' });
+ await expect(client.bulkUpdate(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue);
+
+ expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
+ [options.actions.savedObject.get(type, 'bulk_update')],
+ apiCallOptions.namespace
+ );
+ expect(options.baseClient.bulkUpdate).toHaveBeenCalledWith(objects, apiCallOptions);
+ expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
+ username,
+ 'bulk_update',
+ [type],
+ { objects, options: apiCallOptions }
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
new file mode 100644
index 0000000000000..03b1d770fa770
--- /dev/null
+++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
@@ -0,0 +1,183 @@
+/*
+ * 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 {
+ SavedObjectAttributes,
+ SavedObjectsBaseOptions,
+ SavedObjectsBulkCreateObject,
+ SavedObjectsBulkGetObject,
+ SavedObjectsBulkUpdateObject,
+ SavedObjectsClientContract,
+ SavedObjectsCreateOptions,
+ SavedObjectsFindOptions,
+ SavedObjectsUpdateOptions,
+} from '../../../../../src/core/server';
+import { SecurityAuditLogger } from '../audit';
+import { Actions, CheckSavedObjectsPrivileges } from '../authorization';
+
+interface SecureSavedObjectsClientWrapperOptions {
+ actions: Actions;
+ auditLogger: SecurityAuditLogger;
+ baseClient: SavedObjectsClientContract;
+ errors: SavedObjectsClientContract['errors'];
+ checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges;
+}
+
+export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract {
+ private readonly actions: Actions;
+ private readonly auditLogger: PublicMethodsOf;
+ private readonly baseClient: SavedObjectsClientContract;
+ private readonly checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges;
+ public readonly errors: SavedObjectsClientContract['errors'];
+ constructor({
+ actions,
+ auditLogger,
+ baseClient,
+ checkSavedObjectsPrivilegesAsCurrentUser,
+ errors,
+ }: SecureSavedObjectsClientWrapperOptions) {
+ this.errors = errors;
+ this.actions = actions;
+ this.auditLogger = auditLogger;
+ this.baseClient = baseClient;
+ this.checkSavedObjectsPrivilegesAsCurrentUser = checkSavedObjectsPrivilegesAsCurrentUser;
+ }
+
+ public async create(
+ type: string,
+ attributes: T = {} as T,
+ options: SavedObjectsCreateOptions = {}
+ ) {
+ await this.ensureAuthorized(type, 'create', options.namespace, { type, attributes, options });
+
+ return await this.baseClient.create(type, attributes, options);
+ }
+
+ public async bulkCreate(
+ objects: SavedObjectsBulkCreateObject[],
+ options: SavedObjectsBaseOptions = {}
+ ) {
+ await this.ensureAuthorized(
+ this.getUniqueObjectTypes(objects),
+ 'bulk_create',
+ options.namespace,
+ { objects, options }
+ );
+
+ return await this.baseClient.bulkCreate(objects, options);
+ }
+
+ public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) {
+ await this.ensureAuthorized(type, 'delete', options.namespace, { type, id, options });
+
+ return await this.baseClient.delete(type, id, options);
+ }
+
+ public async find(options: SavedObjectsFindOptions) {
+ await this.ensureAuthorized(options.type, 'find', options.namespace, { options });
+
+ return this.baseClient.find(options);
+ }
+
+ public async bulkGet(
+ objects: SavedObjectsBulkGetObject[] = [],
+ options: SavedObjectsBaseOptions = {}
+ ) {
+ await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_get', options.namespace, {
+ objects,
+ options,
+ });
+
+ return await this.baseClient.bulkGet(objects, options);
+ }
+
+ public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) {
+ await this.ensureAuthorized(type, 'get', options.namespace, { type, id, options });
+
+ return await this.baseClient.get(type, id, options);
+ }
+
+ public async update(
+ type: string,
+ id: string,
+ attributes: Partial,
+ options: SavedObjectsUpdateOptions = {}
+ ) {
+ await this.ensureAuthorized(type, 'update', options.namespace, {
+ type,
+ id,
+ attributes,
+ options,
+ });
+
+ return await this.baseClient.update(type, id, attributes, options);
+ }
+
+ public async bulkUpdate(
+ objects: SavedObjectsBulkUpdateObject[] = [],
+ options: SavedObjectsBaseOptions = {}
+ ) {
+ await this.ensureAuthorized(
+ this.getUniqueObjectTypes(objects),
+ 'bulk_update',
+ options && options.namespace,
+ { objects, options }
+ );
+
+ return await this.baseClient.bulkUpdate(objects, options);
+ }
+
+ private async checkPrivileges(actions: string | string[], namespace?: string) {
+ try {
+ return await this.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespace);
+ } catch (error) {
+ throw this.errors.decorateGeneralError(error, error.body && error.body.reason);
+ }
+ }
+
+ private async ensureAuthorized(
+ typeOrTypes: string | string[],
+ action: string,
+ namespace?: string,
+ args?: Record
+ ) {
+ const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
+ const actionsToTypesMap = new Map(
+ types.map(type => [this.actions.savedObject.get(type, action), type])
+ );
+ const actions = Array.from(actionsToTypesMap.keys());
+ const { hasAllRequested, username, privileges } = await this.checkPrivileges(
+ actions,
+ namespace
+ );
+
+ if (hasAllRequested) {
+ this.auditLogger.savedObjectsAuthorizationSuccess(username, action, types, args);
+ } else {
+ const missingPrivileges = this.getMissingPrivileges(privileges);
+ this.auditLogger.savedObjectsAuthorizationFailure(
+ username,
+ action,
+ types,
+ missingPrivileges,
+ args
+ );
+ const msg = `Unable to ${action} ${missingPrivileges
+ .map(privilege => actionsToTypesMap.get(privilege))
+ .sort()
+ .join(',')}`;
+ throw this.errors.decorateForbiddenError(new Error(msg));
+ }
+ }
+
+ private getMissingPrivileges(privileges: Record) {
+ return Object.keys(privileges).filter(privilege => !privileges[privilege]);
+ }
+
+ private getUniqueObjectTypes(objects: Array<{ type: string }>) {
+ return [...new Set(objects.map(o => o.type))];
+ }
+}
diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json
index 15d900bf99e14..ae121e299cc55 100644
--- a/x-pack/plugins/spaces/kibana.json
+++ b/x-pack/plugins/spaces/kibana.json
@@ -4,6 +4,7 @@
"kibanaVersion": "kibana",
"configPath": ["xpack", "spaces"],
"requiredPlugins": ["features", "licensing"],
+ "optionalPlugins": ["security"],
"server": true,
"ui": false
}
diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts
index 1f20fee46ba4c..2b0cfd3687a24 100644
--- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts
+++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts
@@ -27,9 +27,8 @@ import { SpacesAuditLogger } from '../audit_logger';
import { convertSavedObjectToSpace } from '../../routes/lib';
import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor';
import { Feature } from '../../../../features/server';
-import { OptionalPlugin } from '../../../../../legacy/server/lib/optional_plugin';
-import { SecurityPlugin } from '../../../../../legacy/plugins/security';
import { spacesConfig } from '../__fixtures__';
+import { securityMock } from '../../../../security/server/mocks';
describe('onPostAuthInterceptor', () => {
let root: ReturnType;
@@ -170,7 +169,7 @@ describe('onPostAuthInterceptor', () => {
const spacesService = await service.setup({
http: (http as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetupContract(),
- getSecurity: () => ({} as OptionalPlugin),
+ authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
});
diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts
index e62a3a0efa601..24a994e836e87 100644
--- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts
+++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts
@@ -4,12 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { PluginSetupContract as SecuritySetupContract } from '../../../../security/server';
import { SpacesClient } from './spaces_client';
-import { AuthorizationService } from '../../../../../legacy/plugins/security/server/lib/authorization/service';
-import { actionsFactory } from '../../../../../legacy/plugins/security/server/lib/authorization/actions';
import { ConfigType, ConfigSchema } from '../../config';
import { GetSpacePurpose } from '../../../common/model/types';
+import { securityMock } from '../../../../security/server/mocks';
+
const createMockAuditLogger = () => {
return {
spacesAuthorizationFailure: jest.fn(),
@@ -21,45 +22,17 @@ const createMockDebugLogger = () => {
return jest.fn();
};
-interface MockedAuthorization extends AuthorizationService {
- mode: {
- useRbacForRequest: jest.Mock;
- };
-}
const createMockAuthorization = () => {
const mockCheckPrivilegesAtSpace = jest.fn();
const mockCheckPrivilegesAtSpaces = jest.fn();
const mockCheckPrivilegesGlobally = jest.fn();
- // mocking base path
- const mockConfig = { get: jest.fn().mockReturnValue('/') };
- const mockAuthorization: MockedAuthorization = {
- actions: actionsFactory(mockConfig),
- application: '',
- checkPrivilegesDynamicallyWithRequest: jest.fn().mockImplementation(() => {
- throw new Error(
- 'checkPrivilegesDynamicallyWithRequest should not be called from this test suite'
- );
- }),
- checkSavedObjectsPrivilegesWithRequest: jest.fn().mockImplementation(() => {
- throw new Error(
- 'checkSavedObjectsPrivilegesWithRequest should not be called from this test suite'
- );
- }),
- privileges: {
- get: jest.fn().mockImplementation(() => {
- throw new Error('privileges.get() should not be called from this test suite');
- }),
- },
- checkPrivilegesWithRequest: jest.fn(() => ({
- atSpaces: mockCheckPrivilegesAtSpaces,
- atSpace: mockCheckPrivilegesAtSpace,
- globally: mockCheckPrivilegesGlobally,
- })),
- mode: {
- useRbacForRequest: jest.fn(),
- },
- };
+ const mockAuthorization = securityMock.createSetup().authz;
+ mockAuthorization.checkPrivilegesWithRequest.mockImplementation(() => ({
+ atSpaces: mockCheckPrivilegesAtSpaces,
+ atSpace: mockCheckPrivilegesAtSpace,
+ globally: mockCheckPrivilegesGlobally,
+ }));
return {
mockCheckPrivilegesAtSpaces,
@@ -251,17 +224,17 @@ describe('#getAll', () => {
[
{
purpose: undefined,
- expectedPrivilege: (mockAuthorization: MockedAuthorization) =>
+ expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) =>
mockAuthorization.actions.login,
},
{
purpose: 'any',
- expectedPrivilege: (mockAuthorization: MockedAuthorization) =>
+ expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) =>
mockAuthorization.actions.login,
},
{
purpose: 'copySavedObjectsIntoSpace',
- expectedPrivilege: (mockAuthorization: MockedAuthorization) =>
+ expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) =>
mockAuthorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'),
},
].forEach(scenario => {
diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts
index 052534879e678..f964ae7d7ac32 100644
--- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts
+++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts
@@ -5,22 +5,19 @@
*/
import Boom from 'boom';
import { omit } from 'lodash';
-import { Legacy } from 'kibana';
import { KibanaRequest } from 'src/core/server';
-import { AuthorizationService } from '../../../../../legacy/plugins/security/server/lib/authorization/service';
+import { PluginSetupContract as SecurityPluginSetupContract } from '../../../../security/server';
import { isReservedSpace } from '../../../common/is_reserved_space';
import { Space } from '../../../common/model/space';
import { SpacesAuditLogger } from '../audit_logger';
import { ConfigType } from '../../config';
import { GetSpacePurpose } from '../../../common/model/types';
-type SpacesClientRequestFacade = Legacy.Request | KibanaRequest;
-
const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = ['any', 'copySavedObjectsIntoSpace'];
const PURPOSE_PRIVILEGE_MAP: Record<
GetSpacePurpose,
- (authorization: AuthorizationService) => string
+ (authorization: SecurityPluginSetupContract['authz']) => string
> = {
any: authorization => authorization.actions.login,
copySavedObjectsIntoSpace: authorization =>
@@ -31,11 +28,11 @@ export class SpacesClient {
constructor(
private readonly auditLogger: SpacesAuditLogger,
private readonly debugLogger: (message: string) => void,
- private readonly authorization: AuthorizationService | null,
+ private readonly authorization: SecurityPluginSetupContract['authz'] | null,
private readonly callWithRequestSavedObjectRepository: any,
private readonly config: ConfigType,
private readonly internalSavedObjectRepository: any,
- private readonly request: SpacesClientRequestFacade
+ private readonly request: KibanaRequest
) {}
public async canEnumerateSpaces(): Promise {
@@ -220,10 +217,7 @@ export class SpacesClient {
}
private useRbac(): boolean {
- // TODO: remove "as any" once Security is updated to NP conventions
- return (
- this.authorization != null && this.authorization.mode.useRbacForRequest(this.request as any)
- );
+ return this.authorization != null && this.authorization.mode.useRbacForRequest(this.request);
}
private async ensureAuthorizedGlobally(action: string, method: string, forbiddenMessage: string) {
diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts
index 4fbc4df03d00e..b000c767b53e8 100644
--- a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts
+++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts
@@ -12,9 +12,9 @@ import { SavedObjectsLegacyService } from 'src/core/server';
import { SpacesAuditLogger } from './audit_logger';
import { elasticsearchServiceMock, coreMock } from '../../../../../src/core/server/mocks';
import { spacesServiceMock } from '../spaces_service/spaces_service.mock';
-import { createOptionalPlugin } from '../../../../legacy/server/lib/optional_plugin';
import { LegacyAPI } from '../plugin';
import { spacesConfig } from './__fixtures__';
+import { securityMock } from '../../../security/server/mocks';
const log = {
log: jest.fn(),
@@ -55,8 +55,7 @@ describe('createSpacesTutorialContextFactory', () => {
const spacesService = await service.setup({
http: coreMock.createSetup().http,
elasticsearch: elasticsearchServiceMock.createSetupContract(),
- getSecurity: () =>
- createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
+ authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
});
diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts
index 4b071baaa7e2c..aabdc5bcb97e8 100644
--- a/x-pack/plugins/spaces/server/plugin.ts
+++ b/x-pack/plugins/spaces/server/plugin.ts
@@ -14,10 +14,9 @@ import {
Logger,
PluginInitializerContext,
} from '../../../../src/core/server';
-import { SecurityPlugin } from '../../../legacy/plugins/security';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
+import { PluginSetupContract as SecurityPluginSetup } from '../../security/server';
import { LicensingPluginSetup } from '../../licensing/server';
-import { OptionalPlugin } from '../../../legacy/server/lib/optional_plugin';
import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/xpack_main';
import { createDefaultSpace } from './lib/create_default_space';
// @ts-ignore
@@ -57,14 +56,12 @@ export interface LegacyAPI {
kibanaIndex: string;
};
xpackMain: XPackMainPlugin;
- // TODO: Spaces has a circular dependency with Security right now.
- // Security is not yet available when init runs, so this is wrapped in an optional plugin for the time being.
- security: OptionalPlugin;
}
export interface PluginsSetup {
features: FeaturesPluginSetup;
licensing: LicensingPluginSetup;
+ security?: SecurityPluginSetup;
}
export interface SpacesPluginSetup {
@@ -116,7 +113,7 @@ export class Plugin {
const spacesService = await service.setup({
http: core.http,
elasticsearch: core.elasticsearch,
- getSecurity: () => this.getLegacyAPI().security,
+ authorization: plugins.security ? plugins.security.authz : null,
getSpacesAuditLogger: this.getSpacesAuditLogger,
config$: this.config$,
});
@@ -137,6 +134,10 @@ export class Plugin {
features: plugins.features,
});
+ if (plugins.security) {
+ plugins.security.registerSpacesService(spacesService);
+ }
+
return {
spacesService,
__legacyCompat: {
diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts
index 5f366871ba81e..38a973c1203d5 100644
--- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts
+++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts
@@ -105,7 +105,6 @@ export const createLegacyAPI = ({
},
auditLogger: {} as any,
capabilities: {} as any,
- security: {} as any,
tutorial: {} as any,
usage: {} as any,
xpackMain: {} as any,
diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts
index 54d9654005f89..f25908147bfe5 100644
--- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts
+++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts
@@ -19,13 +19,13 @@ import {
httpServerMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
-import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
import { SpacesClient } from '../../../lib/spaces_client';
import { initCopyToSpacesApi } from './copy_to_space';
import { ObjectType } from '@kbn/config-schema';
import { RouteSchemas } from 'src/core/server/http/router/route';
import { spacesConfig } from '../../../lib/__fixtures__';
+import { securityMock } from '../../../../../security/server/mocks';
describe('copy to space', () => {
const spacesSavedObjects = createSpaces();
@@ -45,8 +45,7 @@ describe('copy to space', () => {
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetupContract(),
- getSecurity: () =>
- createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
+ authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
});
diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts
index e341bd3e4bcbb..86da3023c515e 100644
--- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts
+++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts
@@ -20,13 +20,13 @@ import {
httpServerMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
-import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
import { SpacesClient } from '../../../lib/spaces_client';
import { initDeleteSpacesApi } from './delete';
import { RouteSchemas } from 'src/core/server/http/router/route';
import { ObjectType } from '@kbn/config-schema';
import { spacesConfig } from '../../../lib/__fixtures__';
+import { securityMock } from '../../../../../security/server/mocks';
describe('Spaces Public API', () => {
const spacesSavedObjects = createSpaces();
@@ -46,8 +46,7 @@ describe('Spaces Public API', () => {
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetupContract(),
- getSecurity: () =>
- createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
+ authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
});
diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts
index 69c4f16d4ca80..f9bd4494791f1 100644
--- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts
+++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts
@@ -20,10 +20,10 @@ import {
httpServerMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
-import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
import { SpacesClient } from '../../../lib/spaces_client';
import { spacesConfig } from '../../../lib/__fixtures__';
+import { securityMock } from '../../../../../security/server/mocks';
describe('GET space', () => {
const spacesSavedObjects = createSpaces();
@@ -43,8 +43,7 @@ describe('GET space', () => {
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetupContract(),
- getSecurity: () =>
- createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
+ authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
});
diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts
index fd31b7d084c0e..02219db88a04c 100644
--- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts
+++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts
@@ -19,11 +19,11 @@ import {
httpServerMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
-import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
import { SpacesClient } from '../../../lib/spaces_client';
import { initGetAllSpacesApi } from './get_all';
import { spacesConfig } from '../../../lib/__fixtures__';
+import { securityMock } from '../../../../../security/server/mocks';
describe('GET /spaces/space', () => {
const spacesSavedObjects = createSpaces();
@@ -43,8 +43,7 @@ describe('GET /spaces/space', () => {
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetupContract(),
- getSecurity: () =>
- createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
+ authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
});
diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts
index f874f96833350..398b2e37191b6 100644
--- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts
+++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts
@@ -19,13 +19,13 @@ import {
httpServiceMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
-import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
import { SpacesClient } from '../../../lib/spaces_client';
import { initPostSpacesApi } from './post';
import { RouteSchemas } from 'src/core/server/http/router/route';
import { ObjectType } from '@kbn/config-schema';
import { spacesConfig } from '../../../lib/__fixtures__';
+import { securityMock } from '../../../../../security/server/mocks';
describe('Spaces Public API', () => {
const spacesSavedObjects = createSpaces();
@@ -45,8 +45,7 @@ describe('Spaces Public API', () => {
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetupContract(),
- getSecurity: () =>
- createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
+ authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
});
diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts
index b06bb41fe8b6b..5c213b7f73f62 100644
--- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts
+++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts
@@ -20,13 +20,13 @@ import {
httpServerMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
-import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
import { SpacesClient } from '../../../lib/spaces_client';
import { initPutSpacesApi } from './put';
import { RouteSchemas } from 'src/core/server/http/router/route';
import { ObjectType } from '@kbn/config-schema';
import { spacesConfig } from '../../../lib/__fixtures__';
+import { securityMock } from '../../../../../security/server/mocks';
describe('PUT /api/spaces/space', () => {
const spacesSavedObjects = createSpaces();
@@ -46,8 +46,7 @@ describe('PUT /api/spaces/space', () => {
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetupContract(),
- getSecurity: () =>
- createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
+ authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
});
diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts
index d0910e00586ed..73791201185e8 100644
--- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts
+++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts
@@ -5,7 +5,7 @@
*/
import * as Rx from 'rxjs';
import { SpacesService } from './spaces_service';
-import { coreMock, elasticsearchServiceMock } from 'src/core/server/mocks';
+import { coreMock, elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks';
import { SpacesAuditLogger } from '../lib/audit_logger';
import {
KibanaRequest,
@@ -16,8 +16,8 @@ import {
import { DEFAULT_SPACE_ID } from '../../common/constants';
import { getSpaceIdFromPath } from '../../common/lib/spaces_url_parser';
import { LegacyAPI } from '../plugin';
-import { createOptionalPlugin } from '../../../../legacy/server/lib/optional_plugin';
import { spacesConfig } from '../lib/__fixtures__';
+import { securityMock } from '../../../security/server/mocks';
const mockLogger = {
trace: jest.fn(),
@@ -79,7 +79,7 @@ const createService = async (serverBasePath: string = '') => {
http: httpSetup,
elasticsearch: elasticsearchServiceMock.createSetupContract(),
config$: Rx.of(spacesConfig),
- getSecurity: () => createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
+ authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => new SpacesAuditLogger({}),
});
@@ -183,9 +183,7 @@ describe('SpacesService', () => {
describe('#getActiveSpace', () => {
it('returns the default space when in the default space', async () => {
const spacesServiceSetup = await createService();
- const request = {
- url: { path: 'app/kibana' },
- } as KibanaRequest;
+ const request = httpServerMock.createKibanaRequest({ path: 'app/kibana' });
const activeSpace = await spacesServiceSetup.getActiveSpace(request);
expect(activeSpace).toEqual({
@@ -198,9 +196,7 @@ describe('SpacesService', () => {
it('returns the space for the current (non-default) space', async () => {
const spacesServiceSetup = await createService();
- const request = {
- url: { path: '/s/foo/app/kibana' },
- } as KibanaRequest;
+ const request = httpServerMock.createKibanaRequest({ path: '/s/foo/app/kibana' });
const activeSpace = await spacesServiceSetup.getActiveSpace(request);
expect(activeSpace).toEqual({
@@ -212,11 +208,11 @@ describe('SpacesService', () => {
it('propagates errors from the repository', async () => {
const spacesServiceSetup = await createService();
- const request = {
- url: { path: '/s/unknown-space/app/kibana' },
- } as KibanaRequest;
+ const request = httpServerMock.createKibanaRequest({ path: '/s/unknown-space/app/kibana' });
- expect(spacesServiceSetup.getActiveSpace(request)).rejects.toThrowErrorMatchingInlineSnapshot(
+ await expect(
+ spacesServiceSetup.getActiveSpace(request)
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
`"Saved object [space/unknown-space] not found"`
);
});
diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts
index 83a62f91ade01..b8d0f910a42ea 100644
--- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts
+++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts
@@ -7,9 +7,8 @@
import { map, take } from 'rxjs/operators';
import { Observable, Subscription, combineLatest } from 'rxjs';
import { Legacy } from 'kibana';
-import { Logger, KibanaRequest, CoreSetup } from 'src/core/server';
-import { SecurityPlugin } from '../../../../legacy/plugins/security';
-import { OptionalPlugin } from '../../../../legacy/server/lib/optional_plugin';
+import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server';
+import { PluginSetupContract as SecurityPluginSetup } from '../../../security/server';
import { LegacyAPI } from '../plugin';
import { SpacesClient } from '../lib/spaces_client';
import { ConfigType } from '../config';
@@ -39,7 +38,7 @@ export interface SpacesServiceSetup {
interface SpacesServiceDeps {
http: CoreSetup['http'];
elasticsearch: CoreSetup['elasticsearch'];
- getSecurity: () => OptionalPlugin;
+ authorization: SecurityPluginSetup['authz'] | null;
config$: Observable;
getSpacesAuditLogger(): any;
}
@@ -52,7 +51,7 @@ export class SpacesService {
public async setup({
http,
elasticsearch,
- getSecurity,
+ authorization,
config$,
getSpacesAuditLogger,
}: SpacesServiceDeps): Promise {
@@ -69,7 +68,7 @@ export class SpacesService {
return spaceId;
};
- const getScopedClient = async (request: RequestFacade) => {
+ const getScopedClient = async (request: KibanaRequest) => {
return combineLatest(elasticsearch.adminClient$, config$)
.pipe(
map(([clusterClient, config]) => {
@@ -85,10 +84,6 @@ export class SpacesService {
['space']
);
- const security = getSecurity();
-
- const authorization = security.isEnabled ? security.authorization : null;
-
return new SpacesClient(
getSpacesAuditLogger(),
(message: string) => {
@@ -124,7 +119,9 @@ export class SpacesService {
scopedClient: getScopedClient,
getActiveSpace: async (request: RequestFacade) => {
const spaceId = getSpaceId(request);
- const spacesClient = await getScopedClient(request);
+ const spacesClient = await getScopedClient(
+ request instanceof KibanaRequest ? request : KibanaRequest.from(request)
+ );
return spacesClient.get(spaceId);
},
};
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 28db2197638ff..07076a7fa461d 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -556,7 +556,6 @@
"common.ui.vislib.colormaps.greysText": "グレー",
"common.ui.vislib.colormaps.redsText": "赤",
"common.ui.vislib.colormaps.yellowToRedText": "黄色から赤",
- "common.ui.visualize.dataLoaderError": "ビジュアライゼーションエラー",
"common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "バウンドを取得できませんでした",
"common.ui.welcomeErrorMessage": "Kibana が正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。",
"common.ui.welcomeMessage": "Kibana を読み込み中",
@@ -656,6 +655,20 @@
"core.euiSuperUpdateButton.refreshButtonLabel": "更新",
"core.euiSuperUpdateButton.updateButtonLabel": "更新",
"core.euiSuperUpdateButton.updatingButtonLabel": "更新中",
+ "kibana-react.tableListView.listing.deleteButtonMessage": "{itemCount} 件の {entityName} を削除",
+ "kibana-react.tableListView.listing.deleteConfirmModalDescription": "削除された {entityNamePlural} は復元できません。",
+ "kibana-react.tableListView.listing.deleteSelectedItemsConfirmModal.cancelButtonLabel": "キャンセル",
+ "kibana-react.tableListView.listing.deleteSelectedItemsConfirmModal.confirmButtonLabel": "削除",
+ "kibana-react.tableListView.listing.deleteSelectedItemsConfirmModal.confirmButtonLabelDeleting": "削除中",
+ "kibana-react.tableListView.listing.listingLimitExceeded.advancedSettingsLinkText": "高度な設定",
+ "kibana-react.tableListView.listing.listingLimitExceededDescription": "{totalItems} 件の {entityNamePlural} がありますが、{listingLimitText} の設定により {listingLimitValue} 件までしか下の表に表示できません。この設定は {advancedSettingsLink} で変更できます。",
+ "kibana-react.tableListView.listing.listingLimitExceededTitle": "リスティング制限超過",
+ "kibana-react.tableListView.listing.noAvailableItemsMessage": "利用可能な {entityNamePlural} がありません。",
+ "kibana-react.tableListView.listing.noMatchedItemsMessage": "検索条件に一致する {entityNamePlural} がありません。",
+ "kibana-react.tableListView.listing.table.actionTitle": "アクション",
+ "kibana-react.tableListView.listing.table.editActionDescription": "編集",
+ "kibana-react.tableListView.listing.table.editActionName": "編集",
+ "kibana-react.tableListView.listing.unableToDeleteDangerMessage": "{entityName} を削除できません",
"kibana-react.exitFullScreenButton.exitFullScreenModeButtonAreaLabel": "全画面モードを終了",
"kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel": "全画面を終了",
"kibana-react.exitFullScreenButton.fullScreenModeDescription": "ESC キーで全画面モードを終了します。",
@@ -2371,20 +2384,6 @@
"kbn.server.tutorials.zookeeperMetrics.nameTitle": "Zookeeper メトリック",
"kbn.server.tutorials.zookeeperMetrics.shortDescription": "Zookeeper サーバーから内部メトリックを取得します。",
"kbn.settings.advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other { # オプション}}があります。",
- "kbn.table_list_view.listing.deleteButtonMessage": "{itemCount} 件の {entityName} を削除",
- "kbn.table_list_view.listing.deleteConfirmModalDescription": "削除された {entityNamePlural} は復元できません。",
- "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.cancelButtonLabel": "キャンセル",
- "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.confirmButtonLabel": "削除",
- "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.confirmButtonLabelDeleting": "削除中",
- "kbn.table_list_view.listing.listingLimitExceeded.advancedSettingsLinkText": "高度な設定",
- "kbn.table_list_view.listing.listingLimitExceededDescription": "{totalItems} 件の {entityNamePlural} がありますが、{listingLimitText} の設定により {listingLimitValue} 件までしか下の表に表示できません。この設定は {advancedSettingsLink} で変更できます。",
- "kbn.table_list_view.listing.listingLimitExceededTitle": "リスティング制限超過",
- "kbn.table_list_view.listing.noAvailableItemsMessage": "利用可能な {entityNamePlural} がありません。",
- "kbn.table_list_view.listing.noMatchedItemsMessage": "検索条件に一致する {entityNamePlural} がありません。",
- "kbn.table_list_view.listing.table.actionTitle": "アクション",
- "kbn.table_list_view.listing.table.editActionDescription": "編集",
- "kbn.table_list_view.listing.table.editActionName": "編集",
- "kbn.table_list_view.listing.unableToDeleteDangerMessage": "{entityName} を削除できません",
"kbn.topNavMenu.openInspectorButtonLabel": "検査",
"kbn.topNavMenu.refreshButtonLabel": "更新",
"kbn.topNavMenu.saveVisualizationButtonLabel": "保存",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 6cf125aee8f96..61421acb7dc4a 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -557,7 +557,6 @@
"common.ui.vislib.colormaps.greysText": "灰色",
"common.ui.vislib.colormaps.redsText": "红色",
"common.ui.vislib.colormaps.yellowToRedText": "黄到红",
- "common.ui.visualize.dataLoaderError": "可视化错误",
"common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "无法获取边界",
"common.ui.welcomeErrorMessage": "Kibana 未正确加载。检查服务器输出以了解详情。",
"common.ui.welcomeMessage": "正在加载 Kibana",
@@ -660,6 +659,20 @@
"kibana-react.exitFullScreenButton.exitFullScreenModeButtonAreaLabel": "退出全屏模式",
"kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel": "退出全屏",
"kibana-react.exitFullScreenButton.fullScreenModeDescription": "在全屏模式下,按 ESC 键可退出。",
+ "kibana-react.tableListView.listing.deleteButtonMessage": "删除 {itemCount} 个{entityName}",
+ "kibana-react.tableListView.listing.deleteConfirmModalDescription": "您无法恢复删除的{entityNamePlural}。",
+ "kibana-react.tableListView.listing.deleteSelectedItemsConfirmModal.cancelButtonLabel": "取消",
+ "kibana-react.tableListView.listing.deleteSelectedItemsConfirmModal.confirmButtonLabel": "删除",
+ "kibana-react.tableListView.listing.deleteSelectedItemsConfirmModal.confirmButtonLabelDeleting": "正在删除",
+ "kibana-react.tableListView.listing.listingLimitExceeded.advancedSettingsLinkText": "高级设置",
+ "kibana-react.tableListView.listing.listingLimitExceededDescription": "您有 {totalItems} 个{entityNamePlural},但您的“{listingLimitText}”设置阻止下表显示 {listingLimitValue} 个以上。您可以在“{advancedSettingsLink}”下更改此设置。",
+ "kibana-react.tableListView.listing.listingLimitExceededTitle": "已超过列表限制",
+ "kibana-react.tableListView.listing.noAvailableItemsMessage": "没有可用的{entityNamePlural}",
+ "kibana-react.tableListView.listing.noMatchedItemsMessage": "没有任何{entityNamePlural}匹配您的搜索。",
+ "kibana-react.tableListView.listing.table.actionTitle": "操作",
+ "kibana-react.tableListView.listing.table.editActionDescription": "编辑",
+ "kibana-react.tableListView.listing.table.editActionName": "编辑",
+ "kibana-react.tableListView.listing.unableToDeleteDangerMessage": "无法删除{entityName}",
"inspector.closeButton": "关闭检查器",
"inspector.reqTimestampDescription": "记录请求启动的时间",
"inspector.reqTimestampKey": "请求时间戳",
@@ -2372,20 +2385,6 @@
"kbn.server.tutorials.zookeeperMetrics.nameTitle": "Zookeeper 指标",
"kbn.server.tutorials.zookeeperMetrics.shortDescription": "从 Zookeeper 服务器提取内部指标。",
"kbn.settings.advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您已搜索 {query}。{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}",
- "kbn.table_list_view.listing.deleteButtonMessage": "删除 {itemCount} 个{entityName}",
- "kbn.table_list_view.listing.deleteConfirmModalDescription": "您无法恢复删除的{entityNamePlural}。",
- "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.cancelButtonLabel": "取消",
- "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.confirmButtonLabel": "删除",
- "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.confirmButtonLabelDeleting": "正在删除",
- "kbn.table_list_view.listing.listingLimitExceeded.advancedSettingsLinkText": "高级设置",
- "kbn.table_list_view.listing.listingLimitExceededDescription": "您有 {totalItems} 个{entityNamePlural},但您的“{listingLimitText}”设置阻止下表显示 {listingLimitValue} 个以上。您可以在“{advancedSettingsLink}”下更改此设置。",
- "kbn.table_list_view.listing.listingLimitExceededTitle": "已超过列表限制",
- "kbn.table_list_view.listing.noAvailableItemsMessage": "没有可用的{entityNamePlural}",
- "kbn.table_list_view.listing.noMatchedItemsMessage": "没有任何{entityNamePlural}匹配您的搜索。",
- "kbn.table_list_view.listing.table.actionTitle": "操作",
- "kbn.table_list_view.listing.table.editActionDescription": "编辑",
- "kbn.table_list_view.listing.table.editActionName": "编辑",
- "kbn.table_list_view.listing.unableToDeleteDangerMessage": "无法删除{entityName}",
"kbn.topNavMenu.openInspectorButtonLabel": "检查",
"kbn.topNavMenu.refreshButtonLabel": "刷新",
"kbn.topNavMenu.saveVisualizationButtonLabel": "保存",
diff --git a/x-pack/test/api_integration/apis/management/index_management/settings.js b/x-pack/test/api_integration/apis/management/index_management/settings.js
index bed71c0a62166..dc41f530085b1 100644
--- a/x-pack/test/api_integration/apis/management/index_management/settings.js
+++ b/x-pack/test/api_integration/apis/management/index_management/settings.js
@@ -37,7 +37,6 @@ export default function ({ getService }) {
'max_terms_count',
'lifecycle',
'routing_partition_size',
- 'force_memory_term_dictionary',
'max_docvalue_fields_search',
'merge',
'max_refresh_listeners',
diff --git a/x-pack/test/api_integration/apis/security/builtin_es_privileges.ts b/x-pack/test/api_integration/apis/security/builtin_es_privileges.ts
index cf22394a08616..efce016a16209 100644
--- a/x-pack/test/api_integration/apis/security/builtin_es_privileges.ts
+++ b/x-pack/test/api_integration/apis/security/builtin_es_privileges.ts
@@ -11,10 +11,10 @@ export default function({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('Builtin ES Privileges', () => {
- describe('GET /api/security/v1/esPrivileges/builtin', () => {
+ describe('GET /internal/security/esPrivileges/builtin', () => {
it('should return a list of available builtin privileges', async () => {
await supertest
- .get('/api/security/v1/esPrivileges/builtin')
+ .get('/internal/security/esPrivileges/builtin')
.set('kbn-xsrf', 'xxx')
.send()
.expect(200)
diff --git a/x-pack/test/functional/services/machine_learning/navigation.ts b/x-pack/test/functional/services/machine_learning/navigation.ts
index a55eae9122485..06ab99b3dcb9f 100644
--- a/x-pack/test/functional/services/machine_learning/navigation.ts
+++ b/x-pack/test/functional/services/machine_learning/navigation.ts
@@ -32,12 +32,10 @@ export function MachineLearningNavigationProvider({
},
async navigateToArea(linkSubject: string, pageSubject: string) {
- await retry.tryForTime(2 * 60 * 1000, async () => {
- if ((await testSubjects.exists(`${linkSubject} selected`)) === false) {
- await testSubjects.click(linkSubject);
- await testSubjects.existOrFail(`${linkSubject} selected`, { timeout: 30 * 1000 });
- await testSubjects.existOrFail(pageSubject, { timeout: 30 * 1000 });
- }
+ await testSubjects.click(linkSubject);
+ await retry.tryForTime(60 * 1000, async () => {
+ await testSubjects.existOrFail(`${linkSubject} & ~selected`);
+ await testSubjects.existOrFail(pageSubject);
});
},
@@ -51,11 +49,11 @@ export function MachineLearningNavigationProvider({
},
async navigateToOverview() {
- await this.navigateToArea('mlMainTab overview', 'mlPageOverview');
+ await this.navigateToArea('~mlMainTab & ~overview', 'mlPageOverview');
},
async navigateToAnomalyDetection() {
- await this.navigateToArea('mlMainTab anomalyDetection', 'mlPageJobManagement');
+ await this.navigateToArea('~mlMainTab & ~anomalyDetection', 'mlPageJobManagement');
await this.assertTabsExist('mlSubTab', [
'jobManagement',
'anomalyExplorer',
@@ -65,33 +63,33 @@ export function MachineLearningNavigationProvider({
},
async navigateToDataFrameAnalytics() {
- await this.navigateToArea('mlMainTab dataFrameAnalytics', 'mlPageDataFrameAnalytics');
+ await this.navigateToArea('~mlMainTab & ~dataFrameAnalytics', 'mlPageDataFrameAnalytics');
await this.assertTabsExist('mlSubTab', []);
},
async navigateToDataVisualizer() {
- await this.navigateToArea('mlMainTab dataVisualizer', 'mlPageDataVisualizerSelector');
+ await this.navigateToArea('~mlMainTab & ~dataVisualizer', 'mlPageDataVisualizerSelector');
await this.assertTabsExist('mlSubTab', []);
},
async navigateToJobManagement() {
await this.navigateToAnomalyDetection();
- await this.navigateToArea('mlSubTab jobManagement', 'mlPageJobManagement');
+ await this.navigateToArea('~mlSubTab & ~jobManagement', 'mlPageJobManagement');
},
async navigateToAnomalyExplorer() {
await this.navigateToAnomalyDetection();
- await this.navigateToArea('mlSubTab anomalyExplorer', 'mlPageAnomalyExplorer');
+ await this.navigateToArea('~mlSubTab & ~anomalyExplorer', 'mlPageAnomalyExplorer');
},
async navigateToSingleMetricViewer() {
await this.navigateToAnomalyDetection();
- await this.navigateToArea('mlSubTab singleMetricViewer', 'mlPageSingleMetricViewer');
+ await this.navigateToArea('~mlSubTab & ~singleMetricViewer', 'mlPageSingleMetricViewer');
},
async navigateToSettings() {
await this.navigateToAnomalyDetection();
- await this.navigateToArea('mlSubTab settings', 'mlPageSettings');
+ await this.navigateToArea('~mlSubTab & ~settings', 'mlPageSettings');
},
};
}
diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts
index 3ea00890aedeb..6b15d1bff4209 100644
--- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts
+++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts
@@ -66,9 +66,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
expect(uiCapabilities.value!.catalogue).to.eql(expected);
break;
}
- // if we don't have access at the space itself, we're
- // redirected to the space selector and the ui capabilities
- // are lagely irrelevant because they won't be consumed
+ // if we don't have access at the space itself, security interceptor responds with 404.
case 'no_kibana_privileges at everything_space':
case 'no_kibana_privileges at nothing_space':
case 'legacy_all at everything_space':
@@ -78,9 +76,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
case 'nothing_space_all at everything_space':
case 'nothing_space_read at everything_space':
expect(uiCapabilities.success).to.be(false);
- expect(uiCapabilities.failureReason).to.be(
- GetUICapabilitiesFailureReason.RedirectedToSpaceSelector
- );
+ expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
break;
default:
throw new UnreachableError(scenario);
diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts
index ef3162fe9ddd9..ad4c3582d468f 100644
--- a/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts
+++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts
@@ -70,9 +70,7 @@ export default function fooTests({ getService }: FtrProviderContext) {
show: false,
});
break;
- // if we don't have access at the space itself, we're
- // redirected to the space selector and the ui capabilities
- // are largely irrelevant because they won't be consumed
+ // if we don't have access at the space itself, security interceptor responds with 404.
case 'no_kibana_privileges at everything_space':
case 'no_kibana_privileges at nothing_space':
case 'legacy_all at everything_space':
@@ -82,9 +80,7 @@ export default function fooTests({ getService }: FtrProviderContext) {
case 'nothing_space_all at everything_space':
case 'nothing_space_read at everything_space':
expect(uiCapabilities.success).to.be(false);
- expect(uiCapabilities.failureReason).to.be(
- GetUICapabilitiesFailureReason.RedirectedToSpaceSelector
- );
+ expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
break;
default:
throw new UnreachableError(scenario);
diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts
index 1b9c1daf90282..e9d0cf28e96ec 100644
--- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts
+++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts
@@ -62,6 +62,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
expect(uiCapabilities.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management'));
break;
+ // if we don't have access at the space itself, security interceptor responds with 404.
case 'no_kibana_privileges at everything_space':
case 'no_kibana_privileges at nothing_space':
case 'legacy_all at everything_space':
@@ -71,9 +72,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
case 'nothing_space_all at everything_space':
case 'nothing_space_read at everything_space':
expect(uiCapabilities.success).to.be(false);
- expect(uiCapabilities.failureReason).to.be(
- GetUICapabilitiesFailureReason.RedirectedToSpaceSelector
- );
+ expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
break;
default:
throw new UnreachableError(scenario);
diff --git a/yarn.lock b/yarn.lock
index 93e3dbfaa45ad..71f032cf720dc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1064,10 +1064,10 @@
debug "^3.1.0"
lodash.once "^4.1.1"
-"@elastic/charts@^13.5.12":
- version "13.5.12"
- resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-13.5.12.tgz#95bd92389ec5fb411debfa5979091b6da2e4b123"
- integrity sha512-MMNuebZ5jmzXkUJZr/mSvmtWNIR0gWGBtbqpZBfq3T9WRQPvnEHeE/N1WmXw2BSvwN86fy1i0gr52izh/nfzjQ==
+"@elastic/charts@^14.0.0":
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-14.0.0.tgz#410c87e9ae53df5848aae09a210fa7d08510b376"
+ integrity sha512-cskrD5Yq6yTTqGOKV2/7dw/eRON1GbWkIgSuWXPIBa/TQMUwiWqxFkxSMUJSbu9xXq07KMblDgXLf73yMc0AyQ==
dependencies:
"@types/d3-shape" "^1.3.1"
classnames "^2.2.6"
@@ -14679,11 +14679,6 @@ hoek@6.x.x:
resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.0.3.tgz#7884360426d927865a0a1251fc9c59313af5b798"
integrity sha512-TU6RyZ/XaQCTWRLrdqZZtZqwxUVr6PDMfi6MlWNURZ7A6czanQqX4pFE1mdOUQR9FdPCsZ0UzL8jI/izZ+eBSQ==
-hoist-non-react-statics@^2.3.0:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
- integrity sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w==
-
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0, hoist-non-react-statics@^2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
@@ -23061,18 +23056,6 @@ react-reverse-portal@^1.0.4:
resolved "https://registry.yarnpkg.com/react-reverse-portal/-/react-reverse-portal-1.0.4.tgz#d127d2c9147549b25c4959aba1802eca4b144cd4"
integrity sha512-WESex/wSjxHwdG7M0uwPNkdQXaLauXNHi4INQiRybmFIXVzAqgf/Ak2OzJ4MLf4UuCD/IzEwJOkML2SxnnontA==
-react-router-dom@4.2.2:
- version "4.2.2"
- resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d"
- integrity sha512-cHMFC1ZoLDfEaMFoKTjN7fry/oczMgRt5BKfMAkTu5zEuJvUiPp1J8d0eXSVTnBh6pxlbdqDhozunOOLtmKfPA==
- dependencies:
- history "^4.7.2"
- invariant "^2.2.2"
- loose-envify "^1.3.1"
- prop-types "^15.5.4"
- react-router "^4.2.0"
- warning "^3.0.0"
-
react-router-dom@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6"
@@ -23103,19 +23086,6 @@ react-router@^3.2.0:
prop-types "^15.5.6"
warning "^3.0.0"
-react-router@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986"
- integrity sha512-DY6pjwRhdARE4TDw7XjxjZsbx9lKmIcyZoZ+SDO7SBJ1KUeWNxT22Kara2AC7u6/c2SYEHlEDLnzBCcNhLE8Vg==
- dependencies:
- history "^4.7.2"
- hoist-non-react-statics "^2.3.0"
- invariant "^2.2.2"
- loose-envify "^1.3.1"
- path-to-regexp "^1.7.0"
- prop-types "^15.5.4"
- warning "^3.0.0"
-
react-router@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e"