mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Removing circular dependency between spaces and security (#81891)
* Removing circular dependency between spaces and security * Apply suggestions from code review Co-authored-by: Constance <constancecchen@users.noreply.github.com> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> * Tests refactor - Reorganize top level describes into 3 space-based blocks into based on spaces: - space disabled - spaces plugin unavailable - space enabled (most previous tests go under this new block) with new beforeEach - wrote new tests for uncovered lines 58, 66-69 * Review1: address PR feedback * changing fake requests for alerts/actions * Fixing tests * fixing more tests * Additional testing and refactoring * Apply suggestions from code review Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> * Review 2: Address feedback * Make ESLint happy again Co-authored-by: Constance <constancecchen@users.noreply.github.com> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Constance Chen <constance.chen.3@gmail.com>
This commit is contained in:
parent
1d5701d209
commit
7f962e5839
96 changed files with 2894 additions and 2553 deletions
|
@ -31,7 +31,7 @@ const executeParams = {
|
|||
request: {} as KibanaRequest,
|
||||
};
|
||||
|
||||
const spacesMock = spacesServiceMock.createSetupContract();
|
||||
const spacesMock = spacesServiceMock.createStartContract();
|
||||
const loggerMock = loggingSystemMock.create().get();
|
||||
const getActionsClientWithRequest = jest.fn();
|
||||
actionExecutor.initialize({
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
ProxySettings,
|
||||
} from '../types';
|
||||
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
|
||||
import { SpacesServiceSetup } from '../../../spaces/server';
|
||||
import { SpacesServiceStart } from '../../../spaces/server';
|
||||
import { EVENT_LOG_ACTIONS } from '../plugin';
|
||||
import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
|
||||
import { ActionsClient } from '../actions_client';
|
||||
|
@ -23,7 +23,7 @@ import { ActionExecutionSource } from './action_execution_source';
|
|||
|
||||
export interface ActionExecutorContext {
|
||||
logger: Logger;
|
||||
spaces?: SpacesServiceSetup;
|
||||
spaces?: SpacesServiceStart;
|
||||
getServices: GetServicesFunction;
|
||||
getActionsClientWithRequest: (
|
||||
request: KibanaRequest,
|
||||
|
|
|
@ -12,7 +12,7 @@ import { TaskRunnerFactory } from './task_runner_factory';
|
|||
import { actionTypeRegistryMock } from '../action_type_registry.mock';
|
||||
import { actionExecutorMock } from './action_executor.mock';
|
||||
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { savedObjectsClientMock, loggingSystemMock, httpServiceMock } from 'src/core/server/mocks';
|
||||
import { eventLoggerMock } from '../../../event_log/server/mocks';
|
||||
import { ActionTypeDisabledError } from './errors';
|
||||
import { actionsClientMock } from '../mocks';
|
||||
|
@ -70,7 +70,7 @@ const taskRunnerFactoryInitializerParams = {
|
|||
actionTypeRegistry,
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient,
|
||||
getBasePath: jest.fn().mockReturnValue(undefined),
|
||||
basePathService: httpServiceMock.createBasePath(),
|
||||
getUnsecuredSavedObjectsClient: jest.fn().mockReturnValue(services.savedObjectsClient),
|
||||
};
|
||||
|
||||
|
@ -126,27 +126,23 @@ test('executes the task by calling the executor with proper parameters', async (
|
|||
expect(
|
||||
mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser
|
||||
).toHaveBeenCalledWith('action_task_params', '3', { namespace: 'namespace-test' });
|
||||
|
||||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
actionId: '2',
|
||||
params: { baz: true },
|
||||
request: {
|
||||
getBasePath: expect.any(Function),
|
||||
request: expect.objectContaining({
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
executeParams.request,
|
||||
'/s/test'
|
||||
);
|
||||
});
|
||||
|
||||
test('cleans up action_task_params object', async () => {
|
||||
|
@ -255,24 +251,19 @@ test('uses API key when provided', async () => {
|
|||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
actionId: '2',
|
||||
params: { baz: true },
|
||||
request: {
|
||||
getBasePath: expect.anything(),
|
||||
request: expect.objectContaining({
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
executeParams.request,
|
||||
'/s/test'
|
||||
);
|
||||
});
|
||||
|
||||
test(`doesn't use API key when not provided`, async () => {
|
||||
|
@ -297,21 +288,16 @@ test(`doesn't use API key when not provided`, async () => {
|
|||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
actionId: '2',
|
||||
params: { baz: true },
|
||||
request: {
|
||||
getBasePath: expect.anything(),
|
||||
request: expect.objectContaining({
|
||||
headers: {},
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
executeParams.request,
|
||||
'/s/test'
|
||||
);
|
||||
});
|
||||
|
||||
test(`throws an error when license doesn't support the action type`, async () => {
|
||||
|
|
|
@ -5,14 +5,17 @@
|
|||
*/
|
||||
|
||||
import { pick } from 'lodash';
|
||||
import type { Request } from '@hapi/hapi';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { map, fromNullable, getOrElse } from 'fp-ts/lib/Option';
|
||||
import { addSpaceIdToPath } from '../../../spaces/server';
|
||||
import {
|
||||
Logger,
|
||||
SavedObjectsClientContract,
|
||||
KibanaRequest,
|
||||
SavedObjectReference,
|
||||
} from 'src/core/server';
|
||||
IBasePath,
|
||||
} from '../../../../../src/core/server';
|
||||
import { ActionExecutorContract } from './action_executor';
|
||||
import { ExecutorError } from './executor_error';
|
||||
import { RunContext } from '../../../task_manager/server';
|
||||
|
@ -21,7 +24,6 @@ import { ActionTypeDisabledError } from './errors';
|
|||
import {
|
||||
ActionTaskParams,
|
||||
ActionTypeRegistryContract,
|
||||
GetBasePathFunction,
|
||||
SpaceIdToNamespaceFunction,
|
||||
ActionTypeExecutorResult,
|
||||
} from '../types';
|
||||
|
@ -33,7 +35,7 @@ export interface TaskRunnerContext {
|
|||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
|
||||
spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
getBasePath: GetBasePathFunction;
|
||||
basePathService: IBasePath;
|
||||
getUnsecuredSavedObjectsClient: (request: KibanaRequest) => SavedObjectsClientContract;
|
||||
}
|
||||
|
||||
|
@ -64,7 +66,7 @@ export class TaskRunnerFactory {
|
|||
logger,
|
||||
encryptedSavedObjectsClient,
|
||||
spaceIdToNamespace,
|
||||
getBasePath,
|
||||
basePathService,
|
||||
getUnsecuredSavedObjectsClient,
|
||||
} = this.taskRunnerContext!;
|
||||
|
||||
|
@ -87,11 +89,12 @@ export class TaskRunnerFactory {
|
|||
requestHeaders.authorization = `ApiKey ${apiKey}`;
|
||||
}
|
||||
|
||||
const path = addSpaceIdToPath('/', spaceId);
|
||||
|
||||
// Since we're using API keys and accessing elasticsearch can only be done
|
||||
// via a request, we're faking one with the proper authorization headers.
|
||||
const fakeRequest = ({
|
||||
const fakeRequest = KibanaRequest.from(({
|
||||
headers: requestHeaders,
|
||||
getBasePath: () => getBasePath(spaceId),
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
|
@ -102,7 +105,9 @@ export class TaskRunnerFactory {
|
|||
url: '/',
|
||||
},
|
||||
},
|
||||
} as unknown) as KibanaRequest;
|
||||
} as unknown) as Request);
|
||||
|
||||
basePathService.set(fakeRequest, path);
|
||||
|
||||
let executorResult: ActionTypeExecutorResult<unknown>;
|
||||
try {
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
} from '../../encrypted_saved_objects/server';
|
||||
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
|
||||
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
|
||||
import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server';
|
||||
import { SpacesPluginStart } from '../../spaces/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
|
||||
|
@ -109,7 +109,6 @@ export interface ActionsPluginsSetup {
|
|||
taskManager: TaskManagerSetupContract;
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
spaces?: SpacesPluginSetup;
|
||||
eventLog: IEventLogService;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
|
@ -119,6 +118,7 @@ export interface ActionsPluginsStart {
|
|||
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
|
||||
taskManager: TaskManagerStartContract;
|
||||
licensing: LicensingPluginStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
const includedHiddenTypes = [
|
||||
|
@ -133,12 +133,10 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
|
||||
private readonly logger: Logger;
|
||||
private actionsConfig?: ActionsConfig;
|
||||
private serverBasePath?: string;
|
||||
private taskRunnerFactory?: TaskRunnerFactory;
|
||||
private actionTypeRegistry?: ActionTypeRegistry;
|
||||
private actionExecutor?: ActionExecutor;
|
||||
private licenseState: ILicenseState | null = null;
|
||||
private spaces?: SpacesServiceSetup;
|
||||
private security?: SecurityPluginSetup;
|
||||
private eventLogService?: IEventLogService;
|
||||
private eventLogger?: IEventLogger;
|
||||
|
@ -211,9 +209,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
});
|
||||
this.taskRunnerFactory = taskRunnerFactory;
|
||||
this.actionTypeRegistry = actionTypeRegistry;
|
||||
this.serverBasePath = core.http.basePath.serverBasePath;
|
||||
this.actionExecutor = actionExecutor;
|
||||
this.spaces = plugins.spaces?.spacesService;
|
||||
this.security = plugins.security;
|
||||
|
||||
registerBuiltInActionTypes({
|
||||
|
@ -339,7 +335,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
actionExecutor!.initialize({
|
||||
logger,
|
||||
eventLogger: this.eventLogger!,
|
||||
spaces: this.spaces,
|
||||
spaces: plugins.spaces?.spacesService,
|
||||
getActionsClientWithRequest,
|
||||
getServices: this.getServicesFactory(
|
||||
getScopedSavedObjectsClientWithoutAccessToActions,
|
||||
|
@ -359,12 +355,18 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
: undefined,
|
||||
});
|
||||
|
||||
const spaceIdToNamespace = (spaceId?: string) => {
|
||||
return plugins.spaces && spaceId
|
||||
? plugins.spaces.spacesService.spaceIdToNamespace(spaceId)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
taskRunnerFactory!.initialize({
|
||||
logger,
|
||||
actionTypeRegistry: actionTypeRegistry!,
|
||||
encryptedSavedObjectsClient,
|
||||
getBasePath: this.getBasePath,
|
||||
spaceIdToNamespace: this.spaceIdToNamespace,
|
||||
basePathService: core.http.basePath,
|
||||
spaceIdToNamespace,
|
||||
getUnsecuredSavedObjectsClient: (request: KibanaRequest) =>
|
||||
this.getUnsecuredSavedObjectsClient(core.savedObjects, request),
|
||||
});
|
||||
|
@ -474,14 +476,6 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
};
|
||||
};
|
||||
|
||||
private spaceIdToNamespace = (spaceId?: string): string | undefined => {
|
||||
return this.spaces && spaceId ? this.spaces.spaceIdToNamespace(spaceId) : undefined;
|
||||
};
|
||||
|
||||
private getBasePath = (spaceId?: string): string => {
|
||||
return this.spaces && spaceId ? this.spaces.getBasePath(spaceId) : this.serverBasePath!;
|
||||
};
|
||||
|
||||
public stop() {
|
||||
if (this.licenseState) {
|
||||
this.licenseState.clean();
|
||||
|
|
|
@ -22,7 +22,6 @@ export { ActionTypeExecutorResult } from '../common';
|
|||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
export type GetServicesFunction = (request: KibanaRequest) => Services;
|
||||
export type ActionTypeRegistryContract = PublicMethodsOf<ActionTypeRegistry>;
|
||||
export type GetBasePathFunction = (spaceId?: string) => string;
|
||||
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
|
||||
export type ActionTypeConfig = Record<string, unknown>;
|
||||
export type ActionTypeSecrets = Record<string, unknown>;
|
||||
|
|
|
@ -158,7 +158,6 @@ describe('Alerting Plugin', () => {
|
|||
getActionsClientWithRequest: jest.fn(),
|
||||
getActionsAuthorizationWithRequest: jest.fn(),
|
||||
},
|
||||
spaces: () => null,
|
||||
encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
|
||||
features: mockFeatures(),
|
||||
} as unknown) as AlertingPluginsStart
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
EncryptedSavedObjectsPluginStart,
|
||||
} from '../../encrypted_saved_objects/server';
|
||||
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
|
||||
import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server';
|
||||
import { SpacesPluginStart } from '../../spaces/server';
|
||||
import { AlertsClient } from './alerts_client';
|
||||
import { AlertTypeRegistry } from './alert_type_registry';
|
||||
import { TaskRunnerFactory } from './task_runner';
|
||||
|
@ -101,7 +101,6 @@ export interface AlertingPluginsSetup {
|
|||
actions: ActionsPluginSetupContract;
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
spaces?: SpacesPluginSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
eventLog: IEventLogService;
|
||||
statusService: StatusServiceSetup;
|
||||
|
@ -112,6 +111,7 @@ export interface AlertingPluginsStart {
|
|||
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
|
||||
features: FeaturesPluginStart;
|
||||
eventLog: IEventLogClientService;
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
export class AlertingPlugin {
|
||||
|
@ -119,10 +119,8 @@ export class AlertingPlugin {
|
|||
private readonly logger: Logger;
|
||||
private alertTypeRegistry?: AlertTypeRegistry;
|
||||
private readonly taskRunnerFactory: TaskRunnerFactory;
|
||||
private serverBasePath?: string;
|
||||
private licenseState: LicenseState | null = null;
|
||||
private isESOUsingEphemeralEncryptionKey?: boolean;
|
||||
private spaces?: SpacesServiceSetup;
|
||||
private security?: SecurityPluginSetup;
|
||||
private readonly alertsClientFactory: AlertsClientFactory;
|
||||
private readonly telemetryLogger: Logger;
|
||||
|
@ -151,7 +149,6 @@ export class AlertingPlugin {
|
|||
plugins: AlertingPluginsSetup
|
||||
): Promise<PluginSetupContract> {
|
||||
this.licenseState = new LicenseState(plugins.licensing.license$);
|
||||
this.spaces = plugins.spaces?.spacesService;
|
||||
this.security = plugins.security;
|
||||
|
||||
core.capabilities.registerProvider(() => {
|
||||
|
@ -188,8 +185,6 @@ export class AlertingPlugin {
|
|||
});
|
||||
this.alertTypeRegistry = alertTypeRegistry;
|
||||
|
||||
this.serverBasePath = core.http.basePath.serverBasePath;
|
||||
|
||||
const usageCollection = plugins.usageCollection;
|
||||
if (usageCollection) {
|
||||
initializeAlertingTelemetry(
|
||||
|
@ -261,7 +256,6 @@ export class AlertingPlugin {
|
|||
|
||||
public start(core: CoreStart, plugins: AlertingPluginsStart): PluginStartContract {
|
||||
const {
|
||||
spaces,
|
||||
isESOUsingEphemeralEncryptionKey,
|
||||
logger,
|
||||
taskRunnerFactory,
|
||||
|
@ -274,18 +268,24 @@ export class AlertingPlugin {
|
|||
includedHiddenTypes: ['alert'],
|
||||
});
|
||||
|
||||
const spaceIdToNamespace = (spaceId?: string) => {
|
||||
return plugins.spaces && spaceId
|
||||
? plugins.spaces.spacesService.spaceIdToNamespace(spaceId)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
alertsClientFactory.initialize({
|
||||
alertTypeRegistry: alertTypeRegistry!,
|
||||
logger,
|
||||
taskManager: plugins.taskManager,
|
||||
securityPluginSetup: security,
|
||||
encryptedSavedObjectsClient,
|
||||
spaceIdToNamespace: this.spaceIdToNamespace,
|
||||
spaceIdToNamespace,
|
||||
getSpaceId(request: KibanaRequest) {
|
||||
return spaces?.getSpaceId(request);
|
||||
return plugins.spaces?.spacesService.getSpaceId(request);
|
||||
},
|
||||
async getSpace(request: KibanaRequest) {
|
||||
return spaces?.getActiveSpace(request);
|
||||
return plugins.spaces?.spacesService.getActiveSpace(request);
|
||||
},
|
||||
actions: plugins.actions,
|
||||
features: plugins.features,
|
||||
|
@ -306,10 +306,10 @@ export class AlertingPlugin {
|
|||
logger,
|
||||
getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch),
|
||||
getAlertsClientWithRequest,
|
||||
spaceIdToNamespace: this.spaceIdToNamespace,
|
||||
spaceIdToNamespace,
|
||||
actionsPlugin: plugins.actions,
|
||||
encryptedSavedObjectsClient,
|
||||
getBasePath: this.getBasePath,
|
||||
basePathService: core.http.basePath,
|
||||
eventLogger: this.eventLogger!,
|
||||
internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']),
|
||||
});
|
||||
|
@ -363,14 +363,6 @@ export class AlertingPlugin {
|
|||
});
|
||||
}
|
||||
|
||||
private spaceIdToNamespace = (spaceId?: string): string | undefined => {
|
||||
return this.spaces && spaceId ? this.spaces.spaceIdToNamespace(spaceId) : undefined;
|
||||
};
|
||||
|
||||
private getBasePath = (spaceId?: string): string => {
|
||||
return this.spaces && spaceId ? this.spaces.getBasePath(spaceId) : this.serverBasePath!;
|
||||
};
|
||||
|
||||
private getScopedClientWithAlertSavedObjectType(
|
||||
savedObjects: SavedObjectsServiceStart,
|
||||
request: KibanaRequest
|
||||
|
|
|
@ -18,6 +18,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv
|
|||
import {
|
||||
loggingSystemMock,
|
||||
savedObjectsRepositoryMock,
|
||||
httpServiceMock,
|
||||
} from '../../../../../src/core/server/mocks';
|
||||
import { PluginStartContract as ActionsPluginStart } from '../../../actions/server';
|
||||
import { actionsMock, actionsClientMock } from '../../../actions/server/mocks';
|
||||
|
@ -78,7 +79,7 @@ describe('Task Runner', () => {
|
|||
encryptedSavedObjectsClient,
|
||||
logger: loggingSystemMock.create().get(),
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
getBasePath: jest.fn().mockReturnValue(undefined),
|
||||
basePathService: httpServiceMock.createBasePath(),
|
||||
eventLogger: eventLoggerMock.create(),
|
||||
internalSavedObjectsRepository: savedObjectsRepositoryMock.create(),
|
||||
};
|
||||
|
@ -375,23 +376,24 @@ describe('Task Runner', () => {
|
|||
await taskRunner.run();
|
||||
expect(
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest
|
||||
).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const [
|
||||
request,
|
||||
] = taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mock.calls[0];
|
||||
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
request,
|
||||
'/'
|
||||
);
|
||||
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -768,23 +770,20 @@ describe('Task Runner', () => {
|
|||
});
|
||||
|
||||
await taskRunner.run();
|
||||
expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
const [request] = taskRunnerFactoryInitializerParams.getServices.mock.calls[0];
|
||||
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
request,
|
||||
'/'
|
||||
);
|
||||
});
|
||||
|
||||
test(`doesn't use API key when not provided`, async () => {
|
||||
|
@ -803,20 +802,18 @@ describe('Task Runner', () => {
|
|||
|
||||
await taskRunner.run();
|
||||
|
||||
expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {},
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {},
|
||||
})
|
||||
);
|
||||
|
||||
const [request] = taskRunnerFactoryInitializerParams.getServices.mock.calls[0];
|
||||
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
request,
|
||||
'/'
|
||||
);
|
||||
});
|
||||
|
||||
test('rescheduled the Alert if the schedule has update during a task run', async () => {
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { Dictionary, pickBy, mapValues, without, cloneDeep } from 'lodash';
|
||||
import type { Request } from '@hapi/hapi';
|
||||
import { addSpaceIdToPath } from '../../../spaces/server';
|
||||
import { Logger, KibanaRequest } from '../../../../../src/core/server';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server';
|
||||
|
@ -91,9 +93,10 @@ export class TaskRunner {
|
|||
requestHeaders.authorization = `ApiKey ${apiKey}`;
|
||||
}
|
||||
|
||||
return ({
|
||||
const path = addSpaceIdToPath('/', spaceId);
|
||||
|
||||
const fakeRequest = KibanaRequest.from(({
|
||||
headers: requestHeaders,
|
||||
getBasePath: () => this.context.getBasePath(spaceId),
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
|
@ -104,7 +107,11 @@ export class TaskRunner {
|
|||
url: '/',
|
||||
},
|
||||
},
|
||||
} as unknown) as KibanaRequest;
|
||||
} as unknown) as Request);
|
||||
|
||||
this.context.basePathService.set(fakeRequest, path);
|
||||
|
||||
return fakeRequest;
|
||||
}
|
||||
|
||||
private getServicesWithSpaceLevelPermissions(
|
||||
|
|
|
@ -11,6 +11,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv
|
|||
import {
|
||||
loggingSystemMock,
|
||||
savedObjectsRepositoryMock,
|
||||
httpServiceMock,
|
||||
} from '../../../../../src/core/server/mocks';
|
||||
import { actionsMock } from '../../../actions/server/mocks';
|
||||
import { alertsMock, alertsClientMock } from '../mocks';
|
||||
|
@ -64,7 +65,7 @@ describe('Task Runner Factory', () => {
|
|||
encryptedSavedObjectsClient: encryptedSavedObjectsPlugin.getClient(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
getBasePath: jest.fn().mockReturnValue(undefined),
|
||||
basePathService: httpServiceMock.createBasePath(),
|
||||
eventLogger: eventLoggerMock.create(),
|
||||
internalSavedObjectsRepository: savedObjectsRepositoryMock.create(),
|
||||
};
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { Logger, KibanaRequest, ISavedObjectsRepository } from '../../../../../src/core/server';
|
||||
import {
|
||||
Logger,
|
||||
KibanaRequest,
|
||||
ISavedObjectsRepository,
|
||||
IBasePath,
|
||||
} from '../../../../../src/core/server';
|
||||
import { RunContext } from '../../../task_manager/server';
|
||||
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
|
||||
import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server';
|
||||
import {
|
||||
AlertType,
|
||||
GetBasePathFunction,
|
||||
GetServicesFunction,
|
||||
SpaceIdToNamespaceFunction,
|
||||
} from '../types';
|
||||
import { AlertType, GetServicesFunction, SpaceIdToNamespaceFunction } from '../types';
|
||||
import { TaskRunner } from './task_runner';
|
||||
import { IEventLogger } from '../../../event_log/server';
|
||||
import { AlertsClient } from '../alerts_client';
|
||||
|
@ -26,7 +26,7 @@ export interface TaskRunnerContext {
|
|||
eventLogger: IEventLogger;
|
||||
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
|
||||
spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
getBasePath: GetBasePathFunction;
|
||||
basePathService: IBasePath;
|
||||
internalSavedObjectsRepository: ISavedObjectsRepository;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,6 @@ import {
|
|||
|
||||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
export type GetServicesFunction = (request: KibanaRequest) => Services;
|
||||
export type GetBasePathFunction = (spaceId?: string) => string;
|
||||
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
|
||||
|
||||
declare module 'src/core/server' {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"requiredPlugins": ["features", "licensing"],
|
||||
"configPath": ["enterpriseSearch"],
|
||||
"optionalPlugins": ["usageCollection", "security", "home"],
|
||||
"optionalPlugins": ["usageCollection", "security", "home", "spaces"],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredBundles": ["home"]
|
||||
|
|
|
@ -10,6 +10,19 @@ jest.mock('./enterprise_search_config_api', () => ({
|
|||
import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api';
|
||||
|
||||
import { checkAccess } from './check_access';
|
||||
import { spacesMock } from '../../../spaces/server/mocks';
|
||||
|
||||
const enabledSpace = {
|
||||
id: 'space',
|
||||
name: 'space',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
const disabledSpace = {
|
||||
id: 'space',
|
||||
name: 'space',
|
||||
disabledFeatures: ['enterpriseSearch'],
|
||||
};
|
||||
|
||||
describe('checkAccess', () => {
|
||||
const mockSecurity = {
|
||||
|
@ -29,100 +42,156 @@ describe('checkAccess', () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const mockSpaces = spacesMock.createStart();
|
||||
const mockDependencies = {
|
||||
request: {},
|
||||
request: { auth: { isAuthenticated: true } },
|
||||
config: { host: 'http://localhost:3002' },
|
||||
security: mockSecurity,
|
||||
spaces: mockSpaces,
|
||||
} as any;
|
||||
|
||||
describe('when security is disabled', () => {
|
||||
it('should allow all access', async () => {
|
||||
const security = undefined;
|
||||
expect(await checkAccess({ ...mockDependencies, security })).toEqual({
|
||||
hasAppSearchAccess: true,
|
||||
hasWorkplaceSearchAccess: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is a superuser', () => {
|
||||
it('should allow all access', async () => {
|
||||
const security = {
|
||||
...mockSecurity,
|
||||
authz: {
|
||||
mode: { useRbacForRequest: () => true },
|
||||
checkPrivilegesWithRequest: () => ({
|
||||
globally: () => ({
|
||||
hasAllRequested: true,
|
||||
}),
|
||||
}),
|
||||
actions: { ui: { get: () => {} } },
|
||||
},
|
||||
};
|
||||
expect(await checkAccess({ ...mockDependencies, security })).toEqual({
|
||||
hasAppSearchAccess: true,
|
||||
hasWorkplaceSearchAccess: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to assuming a non-superuser role if auth credentials are missing', async () => {
|
||||
const security = {
|
||||
authz: {
|
||||
...mockSecurity.authz,
|
||||
checkPrivilegesWithRequest: () => ({
|
||||
globally: () => Promise.reject({ statusCode: 403 }),
|
||||
}),
|
||||
},
|
||||
};
|
||||
expect(await checkAccess({ ...mockDependencies, security })).toEqual({
|
||||
describe('when the space is disabled', () => {
|
||||
it('should deny all access', async () => {
|
||||
mockSpaces.spacesService.getActiveSpace.mockResolvedValueOnce(disabledSpace);
|
||||
expect(await checkAccess({ ...mockDependencies })).toEqual({
|
||||
hasAppSearchAccess: false,
|
||||
hasWorkplaceSearchAccess: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws other authz errors', async () => {
|
||||
const security = {
|
||||
authz: {
|
||||
...mockSecurity.authz,
|
||||
checkPrivilegesWithRequest: undefined,
|
||||
},
|
||||
};
|
||||
await expect(checkAccess({ ...mockDependencies, security })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is a non-superuser', () => {
|
||||
describe('when enterpriseSearch.host is not set in kibana.yml', () => {
|
||||
describe('when the spaces plugin is unavailable', () => {
|
||||
describe('when security is disabled', () => {
|
||||
it('should allow all access', async () => {
|
||||
const spaces = undefined;
|
||||
const security = undefined;
|
||||
expect(await checkAccess({ ...mockDependencies, spaces, security })).toEqual({
|
||||
hasAppSearchAccess: true,
|
||||
hasWorkplaceSearchAccess: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getActiveSpace returns 403 forbidden', () => {
|
||||
it('should deny all access', async () => {
|
||||
const config = { host: undefined };
|
||||
expect(await checkAccess({ ...mockDependencies, config })).toEqual({
|
||||
mockSpaces.spacesService.getActiveSpace.mockReturnValueOnce(
|
||||
Promise.reject({ output: { statusCode: 403 } })
|
||||
);
|
||||
expect(await checkAccess({ ...mockDependencies })).toEqual({
|
||||
hasAppSearchAccess: false,
|
||||
hasWorkplaceSearchAccess: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when enterpriseSearch.host is set in kibana.yml', () => {
|
||||
it('should make a http call and return the access response', async () => {
|
||||
(callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({
|
||||
access: {
|
||||
hasAppSearchAccess: false,
|
||||
hasWorkplaceSearchAccess: true,
|
||||
describe('when getActiveSpace throws', () => {
|
||||
it('should re-throw', async () => {
|
||||
mockSpaces.spacesService.getActiveSpace.mockReturnValueOnce(Promise.reject('Error'));
|
||||
let expectedError = '';
|
||||
try {
|
||||
await checkAccess({ ...mockDependencies });
|
||||
} catch (e) {
|
||||
expectedError = e;
|
||||
}
|
||||
expect(expectedError).toEqual('Error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the space is enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockSpaces.spacesService.getActiveSpace.mockResolvedValueOnce(enabledSpace);
|
||||
});
|
||||
|
||||
describe('when security is disabled', () => {
|
||||
it('should allow all access', async () => {
|
||||
const security = undefined;
|
||||
expect(await checkAccess({ ...mockDependencies, security })).toEqual({
|
||||
hasAppSearchAccess: true,
|
||||
hasWorkplaceSearchAccess: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is a superuser', () => {
|
||||
it('should allow all access when enabled at the space ', async () => {
|
||||
const security = {
|
||||
...mockSecurity,
|
||||
authz: {
|
||||
mode: { useRbacForRequest: () => true },
|
||||
checkPrivilegesWithRequest: () => ({
|
||||
globally: () => ({
|
||||
hasAllRequested: true,
|
||||
}),
|
||||
}),
|
||||
actions: { ui: { get: () => {} } },
|
||||
},
|
||||
}));
|
||||
expect(await checkAccess(mockDependencies)).toEqual({
|
||||
hasAppSearchAccess: false,
|
||||
};
|
||||
expect(await checkAccess({ ...mockDependencies, security })).toEqual({
|
||||
hasAppSearchAccess: true,
|
||||
hasWorkplaceSearchAccess: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to no access if no http response', async () => {
|
||||
(callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({}));
|
||||
expect(await checkAccess(mockDependencies)).toEqual({
|
||||
it('falls back to assuming a non-superuser role if auth credentials are missing', async () => {
|
||||
const security = {
|
||||
authz: {
|
||||
...mockSecurity.authz,
|
||||
checkPrivilegesWithRequest: () => ({
|
||||
globally: () => Promise.reject({ statusCode: 403 }),
|
||||
}),
|
||||
},
|
||||
};
|
||||
expect(await checkAccess({ ...mockDependencies, security })).toEqual({
|
||||
hasAppSearchAccess: false,
|
||||
hasWorkplaceSearchAccess: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws other authz errors', async () => {
|
||||
const security = {
|
||||
authz: {
|
||||
...mockSecurity.authz,
|
||||
checkPrivilegesWithRequest: undefined,
|
||||
},
|
||||
};
|
||||
await expect(checkAccess({ ...mockDependencies, security })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is a non-superuser', () => {
|
||||
describe('when enterpriseSearch.host is not set in kibana.yml', () => {
|
||||
it('should deny all access', async () => {
|
||||
const config = { host: undefined };
|
||||
expect(await checkAccess({ ...mockDependencies, config })).toEqual({
|
||||
hasAppSearchAccess: false,
|
||||
hasWorkplaceSearchAccess: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when enterpriseSearch.host is set in kibana.yml', () => {
|
||||
it('should make a http call and return the access response', async () => {
|
||||
(callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({
|
||||
access: {
|
||||
hasAppSearchAccess: false,
|
||||
hasWorkplaceSearchAccess: true,
|
||||
},
|
||||
}));
|
||||
expect(await checkAccess(mockDependencies)).toEqual({
|
||||
hasAppSearchAccess: false,
|
||||
hasWorkplaceSearchAccess: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to no access if no http response', async () => {
|
||||
(callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({}));
|
||||
expect(await checkAccess(mockDependencies)).toEqual({
|
||||
hasAppSearchAccess: false,
|
||||
hasWorkplaceSearchAccess: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { KibanaRequest, Logger } from 'src/core/server';
|
||||
import { SpacesPluginStart } from '../../../spaces/server';
|
||||
import { SecurityPluginSetup } from '../../../security/server';
|
||||
import { ConfigType } from '../';
|
||||
|
||||
|
@ -13,6 +14,7 @@ import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api';
|
|||
interface CheckAccess {
|
||||
request: KibanaRequest;
|
||||
security?: SecurityPluginSetup;
|
||||
spaces?: SpacesPluginStart;
|
||||
config: ConfigType;
|
||||
log: Logger;
|
||||
}
|
||||
|
@ -38,20 +40,53 @@ const DENY_ALL_PLUGINS = {
|
|||
export const checkAccess = async ({
|
||||
config,
|
||||
security,
|
||||
spaces,
|
||||
request,
|
||||
log,
|
||||
}: CheckAccess): Promise<Access> => {
|
||||
const isRbacEnabled = security?.authz.mode.useRbacForRequest(request) ?? false;
|
||||
|
||||
// We can only retrieve the active space when either:
|
||||
// 1) security is enabled, and the request has already been authenticated
|
||||
// 2) security is disabled
|
||||
const attemptSpaceRetrieval = !isRbacEnabled || request.auth.isAuthenticated;
|
||||
|
||||
// If we can't retrieve the current space, then assume the feature is available
|
||||
let allowedAtSpace = false;
|
||||
|
||||
if (!spaces) {
|
||||
allowedAtSpace = true;
|
||||
}
|
||||
|
||||
if (spaces && attemptSpaceRetrieval) {
|
||||
try {
|
||||
const space = await spaces.spacesService.getActiveSpace(request);
|
||||
allowedAtSpace = !space.disabledFeatures?.includes('enterpriseSearch');
|
||||
} catch (err) {
|
||||
if (err?.output?.statusCode === 403) {
|
||||
allowedAtSpace = false;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the plugin if turned off in the current space.
|
||||
if (!allowedAtSpace) {
|
||||
return DENY_ALL_PLUGINS;
|
||||
}
|
||||
|
||||
// If security has been disabled, always show the plugin
|
||||
if (!security?.authz.mode.useRbacForRequest(request)) {
|
||||
if (!isRbacEnabled) {
|
||||
return ALLOW_ALL_PLUGINS;
|
||||
}
|
||||
|
||||
// If the user is a "superuser" or has the base Kibana all privilege globally, always show the plugin
|
||||
const isSuperUser = async (): Promise<boolean> => {
|
||||
try {
|
||||
const { hasAllRequested } = await security.authz
|
||||
const { hasAllRequested } = await security!.authz
|
||||
.checkPrivilegesWithRequest(request)
|
||||
.globally({ kibana: security.authz.actions.ui.get('enterpriseSearch', 'all') });
|
||||
.globally({ kibana: security!.authz.actions.ui.get('enterpriseSearch', 'all') });
|
||||
return hasAllRequested;
|
||||
} catch (err) {
|
||||
if (err.statusCode === 401 || err.statusCode === 403) {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
KibanaRequest,
|
||||
} from 'src/core/server';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { SpacesPluginStart } from '../../spaces/server';
|
||||
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
|
@ -51,6 +52,10 @@ interface PluginsSetup {
|
|||
features: FeaturesPluginSetup;
|
||||
}
|
||||
|
||||
interface PluginsStart {
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
export interface RouteDependencies {
|
||||
router: IRouter;
|
||||
config: ConfigType;
|
||||
|
@ -69,7 +74,7 @@ export class EnterpriseSearchPlugin implements Plugin {
|
|||
}
|
||||
|
||||
public async setup(
|
||||
{ capabilities, http, savedObjects, getStartServices }: CoreSetup,
|
||||
{ capabilities, http, savedObjects, getStartServices }: CoreSetup<PluginsStart>,
|
||||
{ usageCollection, security, features }: PluginsSetup
|
||||
) {
|
||||
const config = await this.config.pipe(first()).toPromise();
|
||||
|
@ -97,7 +102,9 @@ export class EnterpriseSearchPlugin implements Plugin {
|
|||
* Register user access to the Enterprise Search plugins
|
||||
*/
|
||||
capabilities.registerSwitcher(async (request: KibanaRequest) => {
|
||||
const dependencies = { config, security, request, log };
|
||||
const [, { spaces }] = await getStartServices();
|
||||
|
||||
const dependencies = { config, security, spaces, request, log };
|
||||
|
||||
const { hasAppSearchAccess, hasWorkplaceSearchAccess } = await checkAccess(dependencies);
|
||||
const showEnterpriseSearchOverview = hasAppSearchAccess || hasWorkplaceSearchAccess;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { Observable } from 'rxjs';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { LegacyClusterClient, KibanaRequest } from 'src/core/server';
|
||||
import { SpacesServiceSetup } from '../../spaces/server';
|
||||
import { SpacesServiceStart } from '../../spaces/server';
|
||||
|
||||
import { EsContext } from './es';
|
||||
import { IEventLogClient } from './types';
|
||||
|
@ -60,7 +60,7 @@ export type FindOptionsType = Pick<
|
|||
interface EventLogServiceCtorParams {
|
||||
esContext: EsContext;
|
||||
savedObjectGetter: SavedObjectGetter;
|
||||
spacesService?: SpacesServiceSetup;
|
||||
spacesService?: SpacesServiceStart;
|
||||
request: KibanaRequest;
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ interface EventLogServiceCtorParams {
|
|||
export class EventLogClient implements IEventLogClient {
|
||||
private esContext: EsContext;
|
||||
private savedObjectGetter: SavedObjectGetter;
|
||||
private spacesService?: SpacesServiceSetup;
|
||||
private spacesService?: SpacesServiceStart;
|
||||
private request: KibanaRequest;
|
||||
|
||||
constructor({ esContext, savedObjectGetter, spacesService, request }: EventLogServiceCtorParams) {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { Observable } from 'rxjs';
|
||||
import { LegacyClusterClient, KibanaRequest } from 'src/core/server';
|
||||
import { SpacesServiceSetup } from '../../spaces/server';
|
||||
import { SpacesServiceStart } from '../../spaces/server';
|
||||
|
||||
import { EsContext } from './es';
|
||||
import { IEventLogClientService } from './types';
|
||||
|
@ -18,14 +18,14 @@ export type AdminClusterClient$ = Observable<PluginClusterClient>;
|
|||
interface EventLogServiceCtorParams {
|
||||
esContext: EsContext;
|
||||
savedObjectProviderRegistry: SavedObjectProviderRegistry;
|
||||
spacesService?: SpacesServiceSetup;
|
||||
spacesService?: SpacesServiceStart;
|
||||
}
|
||||
|
||||
// note that clusterClient may be null, indicating we can't write to ES
|
||||
export class EventLogClientService implements IEventLogClientService {
|
||||
private esContext: EsContext;
|
||||
private savedObjectProviderRegistry: SavedObjectProviderRegistry;
|
||||
private spacesService?: SpacesServiceSetup;
|
||||
private spacesService?: SpacesServiceStart;
|
||||
|
||||
constructor({
|
||||
esContext,
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
IContextProvider,
|
||||
RequestHandler,
|
||||
} from 'src/core/server';
|
||||
import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server';
|
||||
import { SpacesPluginStart } from '../../spaces/server';
|
||||
|
||||
import {
|
||||
IEventLogConfig,
|
||||
|
@ -41,8 +41,8 @@ const ACTIONS = {
|
|||
stopping: 'stopping',
|
||||
};
|
||||
|
||||
interface PluginSetupDeps {
|
||||
spaces?: SpacesPluginSetup;
|
||||
interface PluginStartDeps {
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
export class Plugin implements CorePlugin<IEventLogService, IEventLogClientService> {
|
||||
|
@ -53,7 +53,6 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
|
|||
private eventLogger?: IEventLogger;
|
||||
private globalConfig$: Observable<SharedGlobalConfig>;
|
||||
private eventLogClientService?: EventLogClientService;
|
||||
private spacesService?: SpacesServiceSetup;
|
||||
private savedObjectProviderRegistry: SavedObjectProviderRegistry;
|
||||
|
||||
constructor(private readonly context: PluginInitializerContext) {
|
||||
|
@ -63,14 +62,13 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
|
|||
this.savedObjectProviderRegistry = new SavedObjectProviderRegistry();
|
||||
}
|
||||
|
||||
async setup(core: CoreSetup, { spaces }: PluginSetupDeps): Promise<IEventLogService> {
|
||||
async setup(core: CoreSetup): Promise<IEventLogService> {
|
||||
const globalConfig = await this.globalConfig$.pipe(first()).toPromise();
|
||||
const kibanaIndex = globalConfig.kibana.index;
|
||||
|
||||
this.systemLogger.debug('setting up plugin');
|
||||
|
||||
const config = await this.config$.pipe(first()).toPromise();
|
||||
this.spacesService = spaces?.spacesService;
|
||||
|
||||
this.esContext = createEsContext({
|
||||
logger: this.systemLogger,
|
||||
|
@ -105,7 +103,7 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
|
|||
return this.eventLogService;
|
||||
}
|
||||
|
||||
async start(core: CoreStart): Promise<IEventLogClientService> {
|
||||
async start(core: CoreStart, { spaces }: PluginStartDeps): Promise<IEventLogClientService> {
|
||||
this.systemLogger.debug('starting plugin');
|
||||
|
||||
if (!this.esContext) throw new Error('esContext not initialized');
|
||||
|
@ -131,7 +129,7 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
|
|||
this.eventLogClientService = new EventLogClientService({
|
||||
esContext: this.esContext,
|
||||
savedObjectProviderRegistry: this.savedObjectProviderRegistry,
|
||||
spacesService: this.spacesService,
|
||||
spacesService: spaces?.spacesService,
|
||||
});
|
||||
return this.eventLogClientService;
|
||||
}
|
||||
|
|
|
@ -5,29 +5,33 @@
|
|||
*/
|
||||
|
||||
import { Legacy } from 'kibana';
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
import { SpacesPluginSetup } from '../../../spaces/server';
|
||||
import { KibanaRequest } from '../../../../../src/core/server';
|
||||
import { SpacesPluginStart } from '../../../spaces/server';
|
||||
|
||||
export type RequestFacade = KibanaRequest | Legacy.Request;
|
||||
|
||||
export function spacesUtilsProvider(
|
||||
spacesPlugin: SpacesPluginSetup | undefined,
|
||||
getSpacesPlugin: (() => Promise<SpacesPluginStart>) | undefined,
|
||||
request: RequestFacade
|
||||
) {
|
||||
async function isMlEnabledInSpace(): Promise<boolean> {
|
||||
if (spacesPlugin === undefined) {
|
||||
if (getSpacesPlugin === undefined) {
|
||||
// if spaces is disabled force isMlEnabledInSpace to be true
|
||||
return true;
|
||||
}
|
||||
const space = await spacesPlugin.spacesService.getActiveSpace(request);
|
||||
const space = await (await getSpacesPlugin()).spacesService.getActiveSpace(
|
||||
request instanceof KibanaRequest ? request : KibanaRequest.from(request)
|
||||
);
|
||||
return space.disabledFeatures.includes('ml') === false;
|
||||
}
|
||||
|
||||
async function getAllSpaces(): Promise<string[] | null> {
|
||||
if (spacesPlugin === undefined) {
|
||||
if (getSpacesPlugin === undefined) {
|
||||
return null;
|
||||
}
|
||||
const client = await spacesPlugin.spacesService.scopedClient(request);
|
||||
const client = (await getSpacesPlugin()).spacesService.createSpacesClient(
|
||||
request instanceof KibanaRequest ? request : KibanaRequest.from(request)
|
||||
);
|
||||
const spaces = await client.getAll();
|
||||
return spaces.map((s) => s.id);
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ import {
|
|||
} from 'kibana/server';
|
||||
import type { SecurityPluginSetup } from '../../security/server';
|
||||
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
|
||||
import { PluginsSetup, PluginsStart, RouteInitialization } from './types';
|
||||
import { SpacesPluginSetup } from '../../spaces/server';
|
||||
import { PluginsSetup, RouteInitialization } from './types';
|
||||
import { PLUGIN_ID } from '../common/constants/app';
|
||||
import { MlCapabilities } from '../common/types/capabilities';
|
||||
|
||||
|
@ -61,7 +61,8 @@ import { RouteGuard } from './lib/route_guard';
|
|||
export type MlPluginSetup = SharedServices;
|
||||
export type MlPluginStart = void;
|
||||
|
||||
export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, PluginsSetup> {
|
||||
export class MlServerPlugin
|
||||
implements Plugin<MlPluginSetup, MlPluginStart, PluginsSetup, PluginsStart> {
|
||||
private log: Logger;
|
||||
private version: string;
|
||||
private mlLicense: MlLicense;
|
||||
|
@ -80,7 +81,7 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
|
|||
this.isMlReady = new Promise((resolve) => (this.setMlReady = resolve));
|
||||
}
|
||||
|
||||
public setup(coreSetup: CoreSetup, plugins: PluginsSetup): MlPluginSetup {
|
||||
public setup(coreSetup: CoreSetup<PluginsStart>, plugins: PluginsSetup): MlPluginSetup {
|
||||
this.spacesPlugin = plugins.spaces;
|
||||
this.security = plugins.security;
|
||||
const { admin, user, apmUser } = getPluginPrivileges();
|
||||
|
@ -157,6 +158,10 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
|
|||
return capabilities.ml as MlCapabilities;
|
||||
};
|
||||
|
||||
const getSpaces = plugins.spaces
|
||||
? () => coreSetup.getStartServices().then(([, { spaces }]) => spaces!)
|
||||
: undefined;
|
||||
|
||||
annotationRoutes(routeInit, plugins.security);
|
||||
calendars(routeInit);
|
||||
dataFeedRoutes(routeInit);
|
||||
|
@ -175,7 +180,7 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
|
|||
jobValidationRoutes(routeInit, this.version);
|
||||
savedObjectsRoutes(routeInit);
|
||||
systemRoutes(routeInit, {
|
||||
spaces: plugins.spaces,
|
||||
getSpaces,
|
||||
cloud: plugins.cloud,
|
||||
resolveMlCapabilities,
|
||||
});
|
||||
|
@ -187,7 +192,7 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
|
|||
return {
|
||||
...createSharedServices(
|
||||
this.mlLicense,
|
||||
plugins.spaces,
|
||||
getSpaces,
|
||||
plugins.cloud,
|
||||
plugins.security?.authz,
|
||||
resolveMlCapabilities,
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { Request } from '@hapi/hapi';
|
||||
import { IScopedClusterClient } from 'kibana/server';
|
||||
import { wrapError } from '../client/error_wrapper';
|
||||
import { mlLog } from '../lib/log';
|
||||
|
@ -19,7 +17,7 @@ import { RouteInitialization, SystemRouteDeps } from '../types';
|
|||
*/
|
||||
export function systemRoutes(
|
||||
{ router, mlLicense, routeGuard }: RouteInitialization,
|
||||
{ spaces, cloud, resolveMlCapabilities }: SystemRouteDeps
|
||||
{ getSpaces, cloud, resolveMlCapabilities }: SystemRouteDeps
|
||||
) {
|
||||
async function getNodeCount(client: IScopedClusterClient) {
|
||||
const { body } = await client.asInternalUser.nodes.info({
|
||||
|
@ -117,7 +115,7 @@ export function systemRoutes(
|
|||
},
|
||||
routeGuard.basicLicenseAPIGuard(async ({ mlClient, request, response }) => {
|
||||
try {
|
||||
const { isMlEnabledInSpace } = spacesUtilsProvider(spaces, (request as unknown) as Request);
|
||||
const { isMlEnabledInSpace } = spacesUtilsProvider(getSpaces, request);
|
||||
|
||||
const mlCapabilities = await resolveMlCapabilities(request);
|
||||
if (mlCapabilities === null) {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { RequestParams } from '@elastic/elasticsearch';
|
|||
import { MlLicense } from '../../../common/license';
|
||||
import { CloudSetup } from '../../../../cloud/server';
|
||||
import { spacesUtilsProvider } from '../../lib/spaces_utils';
|
||||
import { SpacesPluginSetup } from '../../../../spaces/server';
|
||||
import { SpacesPluginStart } from '../../../../spaces/server';
|
||||
import { capabilitiesProvider } from '../../lib/capabilities';
|
||||
import { MlInfoResponse } from '../../../common/types/ml_server_info';
|
||||
import { MlCapabilitiesResponse, ResolveMlCapabilities } from '../../../common/types/capabilities';
|
||||
|
@ -33,7 +33,7 @@ export interface MlSystemProvider {
|
|||
export function getMlSystemProvider(
|
||||
getGuards: GetGuards,
|
||||
mlLicense: MlLicense,
|
||||
spaces: SpacesPluginSetup | undefined,
|
||||
getSpaces: (() => Promise<SpacesPluginStart>) | undefined,
|
||||
cloud: CloudSetup | undefined,
|
||||
resolveMlCapabilities: ResolveMlCapabilities
|
||||
): MlSystemProvider {
|
||||
|
@ -44,7 +44,7 @@ export function getMlSystemProvider(
|
|||
return await getGuards(request, savedObjectsClient)
|
||||
.isMinimumLicense()
|
||||
.ok(async ({ mlClient }) => {
|
||||
const { isMlEnabledInSpace } = spacesUtilsProvider(spaces, request);
|
||||
const { isMlEnabledInSpace } = spacesUtilsProvider(getSpaces, request);
|
||||
|
||||
const mlCapabilities = await resolveMlCapabilities(request);
|
||||
if (mlCapabilities === null) {
|
||||
|
|
|
@ -5,11 +5,8 @@
|
|||
*/
|
||||
|
||||
import { IClusterClient, IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server';
|
||||
import { SpacesPluginSetup } from '../../../spaces/server';
|
||||
// including KibanaRequest from 'kibana/server' causes an error
|
||||
// when being used with instanceof
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { KibanaRequest } from '../../.././../../src/core/server/http';
|
||||
import { SpacesPluginStart } from '../../../spaces/server';
|
||||
import { KibanaRequest } from '../../.././../../src/core/server';
|
||||
import { MlLicense } from '../../common/license';
|
||||
|
||||
import type { CloudSetup } from '../../../cloud/server';
|
||||
|
@ -61,7 +58,7 @@ type OkCallback = (okParams: OkParams) => any;
|
|||
|
||||
export function createSharedServices(
|
||||
mlLicense: MlLicense,
|
||||
spacesPlugin: SpacesPluginSetup | undefined,
|
||||
getSpaces: (() => Promise<SpacesPluginStart>) | undefined,
|
||||
cloud: CloudSetup,
|
||||
authorization: SecurityPluginSetup['authz'] | undefined,
|
||||
resolveMlCapabilities: ResolveMlCapabilities,
|
||||
|
@ -84,7 +81,7 @@ export function createSharedServices(
|
|||
savedObjectsClient,
|
||||
internalSavedObjectsClient,
|
||||
authorization,
|
||||
spacesPlugin !== undefined,
|
||||
getSpaces !== undefined,
|
||||
isMlReady
|
||||
);
|
||||
|
||||
|
@ -119,7 +116,7 @@ export function createSharedServices(
|
|||
...getAnomalyDetectorsProvider(getGuards),
|
||||
...getModulesProvider(getGuards),
|
||||
...getResultsServiceProvider(getGuards),
|
||||
...getMlSystemProvider(getGuards, mlLicense, spacesPlugin, cloud, resolveMlCapabilities),
|
||||
...getMlSystemProvider(getGuards, mlLicense, getSpaces, cloud, resolveMlCapabilities),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import type { CloudSetup } from '../../cloud/server';
|
|||
import type { SecurityPluginSetup } from '../../security/server';
|
||||
import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
import type { LicensingPluginSetup } from '../../licensing/server';
|
||||
import type { SpacesPluginSetup } from '../../spaces/server';
|
||||
import type { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server';
|
||||
import type { MlLicense } from '../common/license';
|
||||
import type { ResolveMlCapabilities } from '../common/types/capabilities';
|
||||
import type { RouteGuard } from './lib/route_guard';
|
||||
|
@ -27,7 +27,7 @@ export interface LicenseCheckResult {
|
|||
|
||||
export interface SystemRouteDeps {
|
||||
cloud: CloudSetup;
|
||||
spaces?: SpacesPluginSetup;
|
||||
getSpaces?: () => Promise<SpacesPluginStart>;
|
||||
resolveMlCapabilities: ResolveMlCapabilities;
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,10 @@ export interface PluginsSetup {
|
|||
usageCollection: UsageCollectionSetup;
|
||||
}
|
||||
|
||||
export interface PluginsStart {
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
export interface RouteInitialization {
|
||||
router: IRouter;
|
||||
mlLicense: MlLicense;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "security"],
|
||||
"requiredPlugins": ["data", "features", "licensing", "taskManager", "securityOss"],
|
||||
"optionalPlugins": ["home", "management", "usageCollection"],
|
||||
"optionalPlugins": ["home", "management", "usageCollection", "spaces"],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredBundles": [
|
||||
|
|
|
@ -114,7 +114,6 @@ describe('Security Plugin', () => {
|
|||
"isEnabled": [Function],
|
||||
"isLicenseAvailable": [Function],
|
||||
},
|
||||
"registerSpacesService": [Function],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
Logger,
|
||||
PluginInitializerContext,
|
||||
} from '../../../../src/core/server';
|
||||
import { SpacesPluginSetup } from '../../spaces/server';
|
||||
import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server';
|
||||
import { PluginSetupContract as FeaturesSetupContract } from '../../features/server';
|
||||
import {
|
||||
PluginSetupContract as FeaturesPluginSetup,
|
||||
|
@ -37,6 +37,7 @@ import { securityFeatures } from './features';
|
|||
import { ElasticsearchService } from './elasticsearch';
|
||||
import { SessionManagementService } from './session_management';
|
||||
import { registerSecurityUsageCollector } from './usage_collector';
|
||||
import { setupSpacesClient } from './spaces';
|
||||
|
||||
export type SpacesService = Pick<
|
||||
SpacesPluginSetup['spacesService'],
|
||||
|
@ -68,16 +69,6 @@ export interface SecurityPluginSetup {
|
|||
>;
|
||||
license: SecurityLicense;
|
||||
audit: AuditServiceSetup;
|
||||
|
||||
/**
|
||||
* If Spaces plugin is available it's supposed to register its SpacesService with Security plugin
|
||||
* so that Security can get space ID from the URL or namespace. We can't declare optional dependency
|
||||
* to Spaces since it'd result into circular dependency between these two plugins and circular
|
||||
* dependencies aren't supported by the Core. In the future we have to get rid of this implicit
|
||||
* dependency.
|
||||
* @param service Spaces service exposed by the Spaces plugin.
|
||||
*/
|
||||
registerSpacesService: (service: SpacesService) => void;
|
||||
}
|
||||
|
||||
export interface PluginSetupDependencies {
|
||||
|
@ -86,12 +77,14 @@ export interface PluginSetupDependencies {
|
|||
taskManager: TaskManagerSetupContract;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
securityOss?: SecurityOssPluginSetup;
|
||||
spaces?: SpacesPluginSetup;
|
||||
}
|
||||
|
||||
export interface PluginStartDependencies {
|
||||
features: FeaturesPluginStart;
|
||||
licensing: LicensingPluginStart;
|
||||
taskManager: TaskManagerStartContract;
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -99,7 +92,6 @@ export interface PluginStartDependencies {
|
|||
*/
|
||||
export class Plugin {
|
||||
private readonly logger: Logger;
|
||||
private spacesService?: SpacesService | symbol = Symbol('not accessed');
|
||||
private securityLicenseService?: SecurityLicenseService;
|
||||
private authc?: Authentication;
|
||||
|
||||
|
@ -121,22 +113,20 @@ export class Plugin {
|
|||
this.initializerContext.logger.get('session')
|
||||
);
|
||||
|
||||
private readonly getSpacesService = () => {
|
||||
// Changing property value from Symbol to undefined denotes the fact that property was accessed.
|
||||
if (!this.wasSpacesServiceAccessed()) {
|
||||
this.spacesService = undefined;
|
||||
}
|
||||
|
||||
return this.spacesService as SpacesService | undefined;
|
||||
};
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.logger = this.initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public async setup(
|
||||
core: CoreSetup<PluginStartDependencies>,
|
||||
{ features, licensing, taskManager, usageCollection, securityOss }: PluginSetupDependencies
|
||||
{
|
||||
features,
|
||||
licensing,
|
||||
taskManager,
|
||||
usageCollection,
|
||||
securityOss,
|
||||
spaces,
|
||||
}: PluginSetupDependencies
|
||||
) {
|
||||
const [config, legacyConfig] = await combineLatest([
|
||||
this.initializerContext.config.create<TypeOf<typeof ConfigSchema>>().pipe(
|
||||
|
@ -182,7 +172,7 @@ export class Plugin {
|
|||
config: config.audit,
|
||||
logging: core.logging,
|
||||
http: core.http,
|
||||
getSpaceId: (request) => this.getSpacesService()?.getSpaceId(request),
|
||||
getSpaceId: (request) => spaces?.spacesService.getSpaceId(request),
|
||||
getCurrentUser: (request) => this.authc?.getCurrentUser(request),
|
||||
});
|
||||
const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger());
|
||||
|
@ -216,17 +206,23 @@ export class Plugin {
|
|||
kibanaIndexName: legacyConfig.kibana.index,
|
||||
packageVersion: this.initializerContext.env.packageInfo.version,
|
||||
buildNumber: this.initializerContext.env.packageInfo.buildNum,
|
||||
getSpacesService: this.getSpacesService,
|
||||
getSpacesService: () => spaces?.spacesService,
|
||||
features,
|
||||
getCurrentUser: this.authc.getCurrentUser,
|
||||
});
|
||||
|
||||
setupSpacesClient({
|
||||
spaces,
|
||||
audit,
|
||||
authz,
|
||||
});
|
||||
|
||||
setupSavedObjects({
|
||||
legacyAuditLogger,
|
||||
audit,
|
||||
authz,
|
||||
savedObjects: core.savedObjects,
|
||||
getSpacesService: this.getSpacesService,
|
||||
getSpacesService: () => spaces?.spacesService,
|
||||
});
|
||||
|
||||
defineRoutes({
|
||||
|
@ -271,14 +267,6 @@ export class Plugin {
|
|||
},
|
||||
|
||||
license,
|
||||
|
||||
registerSpacesService: (service) => {
|
||||
if (this.wasSpacesServiceAccessed()) {
|
||||
throw new Error('Spaces service has been accessed before registration.');
|
||||
}
|
||||
|
||||
this.spacesService = service;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -312,8 +300,4 @@ export class Plugin {
|
|||
this.elasticsearchService.stop();
|
||||
this.sessionManagementService.stop();
|
||||
}
|
||||
|
||||
private wasSpacesServiceAccessed() {
|
||||
return typeof this.spacesService !== 'symbol';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
import { definePrivilegesRoutes } from './privileges';
|
||||
import { defineRolesRoutes } from './roles';
|
||||
import { resetSessionPageRoutes } from './reset_session_page';
|
||||
import { defineShareSavedObjectPermissionRoutes } from './spaces';
|
||||
import { RouteDefinitionParams } from '..';
|
||||
|
||||
export function defineAuthorizationRoutes(params: RouteDefinitionParams) {
|
||||
defineRolesRoutes(params);
|
||||
definePrivilegesRoutes(params);
|
||||
resetSessionPageRoutes(params);
|
||||
defineShareSavedObjectPermissionRoutes(params);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { defineShareSavedObjectPermissionRoutes } from './share_saved_object_permissions';
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 {
|
||||
IRouter,
|
||||
kibanaResponseFactory,
|
||||
RequestHandler,
|
||||
RequestHandlerContext,
|
||||
RouteConfig,
|
||||
} from '../../../../../../../src/core/server';
|
||||
import { defineShareSavedObjectPermissionRoutes } from './share_saved_object_permissions';
|
||||
|
||||
import { httpServerMock } from '../../../../../../../src/core/server/mocks';
|
||||
import { routeDefinitionParamsMock } from '../../index.mock';
|
||||
import { RouteDefinitionParams } from '../..';
|
||||
import { DeeplyMockedKeys } from '@kbn/utility-types/target/jest';
|
||||
import { CheckPrivileges } from '../../../authorization/types';
|
||||
|
||||
describe('Share Saved Object Permissions', () => {
|
||||
let router: jest.Mocked<IRouter>;
|
||||
let routeParamsMock: DeeplyMockedKeys<RouteDefinitionParams>;
|
||||
|
||||
const mockContext = ({
|
||||
licensing: {
|
||||
license: { check: jest.fn().mockReturnValue({ state: 'valid' }) },
|
||||
},
|
||||
} as unknown) as RequestHandlerContext;
|
||||
|
||||
beforeEach(() => {
|
||||
routeParamsMock = routeDefinitionParamsMock.create();
|
||||
router = routeParamsMock.router as jest.Mocked<IRouter>;
|
||||
|
||||
defineShareSavedObjectPermissionRoutes(routeParamsMock);
|
||||
});
|
||||
|
||||
describe('GET /internal/security/_share_saved_object_permissions', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
let routeConfig: RouteConfig<any, any, any, any>;
|
||||
beforeEach(() => {
|
||||
const [shareRouteConfig, shareRouteHandler] = router.get.mock.calls.find(
|
||||
([{ path }]) => path === '/internal/security/_share_saved_object_permissions'
|
||||
)!;
|
||||
|
||||
routeConfig = shareRouteConfig;
|
||||
routeHandler = shareRouteHandler;
|
||||
});
|
||||
|
||||
it('correctly defines route.', () => {
|
||||
expect(routeConfig.options).toBeUndefined();
|
||||
expect(routeConfig.validate).toHaveProperty('query');
|
||||
});
|
||||
|
||||
it('returns `true` when the user is authorized globally', async () => {
|
||||
const checkPrivilegesWithRequest = jest.fn().mockResolvedValue({ hasAllRequested: true });
|
||||
|
||||
routeParamsMock.authz.checkPrivilegesWithRequest.mockReturnValue(({
|
||||
globally: checkPrivilegesWithRequest,
|
||||
} as unknown) as CheckPrivileges);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
query: {
|
||||
type: 'foo-type',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
routeHandler(mockContext, request, kibanaResponseFactory)
|
||||
).resolves.toMatchObject({
|
||||
status: 200,
|
||||
payload: {
|
||||
shareToAllSpaces: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(routeParamsMock.authz.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
expect(checkPrivilegesWithRequest).toHaveBeenCalledTimes(1);
|
||||
expect(checkPrivilegesWithRequest).toHaveBeenCalledWith({
|
||||
kibana: routeParamsMock.authz.actions.savedObject.get('foo-type', 'share-to-space'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns `false` when the user is not authorized globally', async () => {
|
||||
const checkPrivilegesWithRequest = jest.fn().mockResolvedValue({ hasAllRequested: false });
|
||||
|
||||
routeParamsMock.authz.checkPrivilegesWithRequest.mockReturnValue(({
|
||||
globally: checkPrivilegesWithRequest,
|
||||
} as unknown) as CheckPrivileges);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
query: {
|
||||
type: 'foo-type',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
routeHandler(mockContext, request, kibanaResponseFactory)
|
||||
).resolves.toMatchObject({
|
||||
status: 200,
|
||||
payload: {
|
||||
shareToAllSpaces: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(routeParamsMock.authz.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
expect(checkPrivilegesWithRequest).toHaveBeenCalledTimes(1);
|
||||
expect(checkPrivilegesWithRequest).toHaveBeenCalledWith({
|
||||
kibana: routeParamsMock.authz.actions.savedObject.get('foo-type', 'share-to-space'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { RouteDefinitionParams } from '../../index';
|
||||
import { createLicensedRouteHandler } from '../../licensed_route_handler';
|
||||
import { wrapIntoCustomErrorResponse } from '../../../errors';
|
||||
|
||||
export function defineShareSavedObjectPermissionRoutes({ router, authz }: RouteDefinitionParams) {
|
||||
router.get(
|
||||
{
|
||||
path: '/internal/security/_share_saved_object_permissions',
|
||||
validate: { query: schema.object({ type: schema.string() }) },
|
||||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
let shareToAllSpaces = true;
|
||||
const { type } = request.query;
|
||||
|
||||
try {
|
||||
const checkPrivileges = authz.checkPrivilegesWithRequest(request);
|
||||
shareToAllSpaces = (
|
||||
await checkPrivileges.globally({
|
||||
kibana: authz.actions.savedObject.get(type, 'share_to_space'),
|
||||
})
|
||||
).hasAllRequested;
|
||||
} catch (error) {
|
||||
return response.customError(wrapIntoCustomErrorResponse(error));
|
||||
}
|
||||
return response.ok({ body: { shareToAllSpaces } });
|
||||
})
|
||||
);
|
||||
}
|
|
@ -15,23 +15,26 @@ import { authorizationMock } from '../authorization/index.mock';
|
|||
import { ConfigSchema, createConfig } from '../config';
|
||||
import { licenseMock } from '../../common/licensing/index.mock';
|
||||
import { sessionMock } from '../session_management/session.mock';
|
||||
import { RouteDefinitionParams } from '.';
|
||||
import { DeeplyMockedKeys } from '@kbn/utility-types/jest';
|
||||
|
||||
export const routeDefinitionParamsMock = {
|
||||
create: (config: Record<string, unknown> = {}) => ({
|
||||
router: httpServiceMock.createRouter(),
|
||||
basePath: httpServiceMock.createBasePath(),
|
||||
csp: httpServiceMock.createSetupContract().csp,
|
||||
logger: loggingSystemMock.create().get(),
|
||||
clusterClient: elasticsearchServiceMock.createLegacyClusterClient(),
|
||||
config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get(), {
|
||||
isTLSEnabled: false,
|
||||
}),
|
||||
authc: authenticationMock.create(),
|
||||
authz: authorizationMock.create(),
|
||||
license: licenseMock.create(),
|
||||
httpResources: httpResourcesMock.createRegistrar(),
|
||||
getFeatures: jest.fn(),
|
||||
getFeatureUsageService: jest.fn(),
|
||||
session: sessionMock.create(),
|
||||
}),
|
||||
create: (config: Record<string, unknown> = {}) =>
|
||||
(({
|
||||
router: httpServiceMock.createRouter(),
|
||||
basePath: httpServiceMock.createBasePath(),
|
||||
csp: httpServiceMock.createSetupContract().csp,
|
||||
logger: loggingSystemMock.create().get(),
|
||||
clusterClient: elasticsearchServiceMock.createLegacyClusterClient(),
|
||||
config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get(), {
|
||||
isTLSEnabled: false,
|
||||
}),
|
||||
authc: authenticationMock.create(),
|
||||
authz: authorizationMock.create(),
|
||||
license: licenseMock.create(),
|
||||
httpResources: httpResourcesMock.createRegistrar(),
|
||||
getFeatures: jest.fn(),
|
||||
getFeatureUsageService: jest.fn(),
|
||||
session: sessionMock.create(),
|
||||
} as unknown) as DeeplyMockedKeys<RouteDefinitionParams>),
|
||||
};
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { SpacesClient } from './spaces_client';
|
||||
export { setupSpacesClient } from './setup_spaces_client';
|
|
@ -3,7 +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 { SpacesAuditLogger } from './audit_logger';
|
||||
import { LegacySpacesAuditLogger } from './legacy_audit_logger';
|
||||
|
||||
const createMockAuditLogger = () => {
|
||||
return {
|
||||
|
@ -14,7 +14,7 @@ const createMockAuditLogger = () => {
|
|||
describe(`#savedObjectsAuthorizationFailure`, () => {
|
||||
test('logs auth failure with spaceIds via auditLogger', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const securityAuditLogger = new SpacesAuditLogger(auditLogger);
|
||||
const securityAuditLogger = new LegacySpacesAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const action = 'foo-action';
|
||||
const spaceIds = ['foo-space-1', 'foo-space-2'];
|
||||
|
@ -34,7 +34,7 @@ describe(`#savedObjectsAuthorizationFailure`, () => {
|
|||
|
||||
test('logs auth failure without spaceIds via auditLogger', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const securityAuditLogger = new SpacesAuditLogger(auditLogger);
|
||||
const securityAuditLogger = new LegacySpacesAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const action = 'foo-action';
|
||||
|
||||
|
@ -54,7 +54,7 @@ describe(`#savedObjectsAuthorizationFailure`, () => {
|
|||
describe(`#savedObjectsAuthorizationSuccess`, () => {
|
||||
test('logs auth success with spaceIds via auditLogger', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const securityAuditLogger = new SpacesAuditLogger(auditLogger);
|
||||
const securityAuditLogger = new LegacySpacesAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const action = 'foo-action';
|
||||
const spaceIds = ['foo-space-1', 'foo-space-2'];
|
||||
|
@ -74,7 +74,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => {
|
|||
|
||||
test('logs auth success without spaceIds via auditLogger', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const securityAuditLogger = new SpacesAuditLogger(auditLogger);
|
||||
const securityAuditLogger = new LegacySpacesAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const action = 'foo-action';
|
||||
|
|
@ -4,14 +4,24 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { LegacyAuditLogger } from '../../../security/server';
|
||||
import { LegacyAuditLogger } from '../audit';
|
||||
|
||||
export class SpacesAuditLogger {
|
||||
/**
|
||||
* @deprecated will be removed in 8.0
|
||||
*/
|
||||
export class LegacySpacesAuditLogger {
|
||||
private readonly auditLogger: LegacyAuditLogger;
|
||||
|
||||
/**
|
||||
* @deprecated will be removed in 8.0
|
||||
*/
|
||||
constructor(auditLogger: LegacyAuditLogger = { log() {} }) {
|
||||
this.auditLogger = auditLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated will be removed in 8.0
|
||||
*/
|
||||
public spacesAuthorizationFailure(username: string, action: string, spaceIds?: string[]) {
|
||||
this.auditLogger.log(
|
||||
'spaces_authorization_failure',
|
||||
|
@ -24,6 +34,9 @@ export class SpacesAuditLogger {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated will be removed in 8.0
|
||||
*/
|
||||
public spacesAuthorizationSuccess(username: string, action: string, spaceIds?: string[]) {
|
||||
this.auditLogger.log(
|
||||
'spaces_authorization_success',
|
|
@ -0,0 +1,623 @@
|
|||
/*
|
||||
* 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 { httpServerMock } from '../../../../../src/core/server/mocks';
|
||||
|
||||
import { SecureSpacesClientWrapper } from './secure_spaces_client_wrapper';
|
||||
|
||||
import { spacesClientMock } from '../../../spaces/server/mocks';
|
||||
import { deepFreeze } from '@kbn/std';
|
||||
import { Space } from '../../../spaces/server';
|
||||
import { authorizationMock } from '../authorization/index.mock';
|
||||
import { AuthorizationServiceSetup } from '../authorization';
|
||||
import { GetAllSpacesPurpose } from '../../../spaces/common/model/types';
|
||||
import { CheckPrivilegesResponse } from '../authorization/types';
|
||||
import { LegacySpacesAuditLogger } from './legacy_audit_logger';
|
||||
import { SavedObjectsErrorHelpers } from 'src/core/server';
|
||||
|
||||
interface Opts {
|
||||
securityEnabled?: boolean;
|
||||
}
|
||||
|
||||
const spaces = (deepFreeze([
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default Space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
name: 'Marketing Space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'sales',
|
||||
name: 'Sales Space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
]) as unknown) as Space[];
|
||||
|
||||
const setup = ({ securityEnabled = false }: Opts = {}) => {
|
||||
const baseClient = spacesClientMock.create();
|
||||
baseClient.getAll.mockResolvedValue([...spaces]);
|
||||
|
||||
baseClient.get.mockImplementation(async (spaceId: string) => {
|
||||
const space = spaces.find((s) => s.id === spaceId);
|
||||
if (!space) {
|
||||
throw SavedObjectsErrorHelpers.createGenericNotFoundError('space', spaceId);
|
||||
}
|
||||
return space;
|
||||
});
|
||||
|
||||
const authorization = authorizationMock.create({
|
||||
version: 'unit-test',
|
||||
applicationName: 'kibana',
|
||||
});
|
||||
authorization.mode.useRbacForRequest.mockReturnValue(securityEnabled);
|
||||
|
||||
const legacyAuditLogger = ({
|
||||
spacesAuthorizationFailure: jest.fn(),
|
||||
spacesAuthorizationSuccess: jest.fn(),
|
||||
} as unknown) as jest.Mocked<LegacySpacesAuditLogger>;
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const wrapper = new SecureSpacesClientWrapper(
|
||||
baseClient,
|
||||
request,
|
||||
authorization,
|
||||
legacyAuditLogger
|
||||
);
|
||||
return {
|
||||
authorization,
|
||||
wrapper,
|
||||
request,
|
||||
baseClient,
|
||||
legacyAuditLogger,
|
||||
};
|
||||
};
|
||||
|
||||
const expectNoAuthorizationCheck = (authorization: jest.Mocked<AuthorizationServiceSetup>) => {
|
||||
expect(authorization.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled();
|
||||
expect(authorization.checkPrivilegesWithRequest).not.toHaveBeenCalled();
|
||||
expect(authorization.checkSavedObjectsPrivilegesWithRequest).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
const expectNoAuditLogging = (auditLogger: jest.Mocked<LegacySpacesAuditLogger>) => {
|
||||
expect(auditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(auditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
const expectForbiddenAuditLogging = (
|
||||
auditLogger: jest.Mocked<LegacySpacesAuditLogger>,
|
||||
username: string,
|
||||
operation: string,
|
||||
spaceId?: string
|
||||
) => {
|
||||
expect(auditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
if (spaceId) {
|
||||
expect(auditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, operation, [
|
||||
spaceId,
|
||||
]);
|
||||
} else {
|
||||
expect(auditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, operation);
|
||||
}
|
||||
|
||||
expect(auditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
const expectSuccessAuditLogging = (
|
||||
auditLogger: jest.Mocked<LegacySpacesAuditLogger>,
|
||||
username: string,
|
||||
operation: string,
|
||||
spaceIds?: string[]
|
||||
) => {
|
||||
expect(auditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(1);
|
||||
if (spaceIds) {
|
||||
expect(auditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(
|
||||
username,
|
||||
operation,
|
||||
spaceIds
|
||||
);
|
||||
} else {
|
||||
expect(auditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, operation);
|
||||
}
|
||||
|
||||
expect(auditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
describe('SecureSpacesClientWrapper', () => {
|
||||
describe('#getAll', () => {
|
||||
const savedObjects = [
|
||||
{
|
||||
id: 'default',
|
||||
attributes: {
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
attributes: {
|
||||
name: 'bar-name',
|
||||
description: 'bar-description',
|
||||
bar: 'bar-bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sales',
|
||||
attributes: {
|
||||
name: 'bar-name',
|
||||
description: 'bar-description',
|
||||
bar: 'bar-bar',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('delegates to base client when security is not enabled', async () => {
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger } = setup({
|
||||
securityEnabled: false,
|
||||
});
|
||||
|
||||
const response = await wrapper.getAll();
|
||||
expect(baseClient.getAll).toHaveBeenCalledTimes(1);
|
||||
expect(baseClient.getAll).toHaveBeenCalledWith({ purpose: 'any' });
|
||||
expect(response).toEqual(spaces);
|
||||
expectNoAuthorizationCheck(authorization);
|
||||
expectNoAuditLogging(legacyAuditLogger);
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
purpose: undefined,
|
||||
expectedPrivilege: (mockAuthorization: AuthorizationServiceSetup) => [
|
||||
mockAuthorization.actions.login,
|
||||
],
|
||||
},
|
||||
{
|
||||
purpose: 'any' as GetAllSpacesPurpose,
|
||||
expectedPrivilege: (mockAuthorization: AuthorizationServiceSetup) => [
|
||||
mockAuthorization.actions.login,
|
||||
],
|
||||
},
|
||||
{
|
||||
purpose: 'copySavedObjectsIntoSpace' as GetAllSpacesPurpose,
|
||||
expectedPrivilege: (mockAuthorization: AuthorizationServiceSetup) => [
|
||||
mockAuthorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'),
|
||||
],
|
||||
},
|
||||
{
|
||||
purpose: 'findSavedObjects' as GetAllSpacesPurpose,
|
||||
expectedPrivilege: (mockAuthorization: AuthorizationServiceSetup) => [
|
||||
mockAuthorization.actions.login,
|
||||
mockAuthorization.actions.savedObject.get('config', 'find'),
|
||||
],
|
||||
},
|
||||
{
|
||||
purpose: 'shareSavedObjectsIntoSpace' as GetAllSpacesPurpose,
|
||||
expectedPrivilege: (mockAuthorization: AuthorizationServiceSetup) => [
|
||||
mockAuthorization.actions.ui.get('savedObjectsManagement', 'shareIntoSpace'),
|
||||
],
|
||||
},
|
||||
].forEach((scenario) => {
|
||||
describe(`with purpose='${scenario.purpose}'`, () => {
|
||||
test(`throws Boom.forbidden when user isn't authorized for any spaces`, async () => {
|
||||
const username = 'some-user';
|
||||
const { authorization, wrapper, baseClient, request, legacyAuditLogger } = setup({
|
||||
securityEnabled: true,
|
||||
});
|
||||
|
||||
const privileges = scenario.expectedPrivilege(authorization);
|
||||
|
||||
const checkPrivileges = jest.fn().mockResolvedValue({
|
||||
username,
|
||||
privileges: {
|
||||
kibana: [
|
||||
...privileges
|
||||
.map((privilege) => [
|
||||
{ resource: savedObjects[0].id, privilege, authorized: false },
|
||||
{ resource: savedObjects[1].id, privilege, authorized: false },
|
||||
])
|
||||
.flat(),
|
||||
],
|
||||
},
|
||||
} as CheckPrivilegesResponse);
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({ atSpaces: checkPrivileges });
|
||||
|
||||
await expect(wrapper.getAll({ purpose: scenario.purpose })).rejects.toThrowError(
|
||||
'Forbidden'
|
||||
);
|
||||
|
||||
expect(baseClient.getAll).toHaveBeenCalledWith({ purpose: scenario.purpose ?? 'any' });
|
||||
expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
expect(checkPrivileges).toHaveBeenCalledWith(
|
||||
savedObjects.map((savedObject) => savedObject.id),
|
||||
{ kibana: privileges }
|
||||
);
|
||||
|
||||
expectForbiddenAuditLogging(legacyAuditLogger, username, 'getAll');
|
||||
});
|
||||
|
||||
test(`returns spaces that the user is authorized for`, async () => {
|
||||
const username = 'some-user';
|
||||
const { authorization, wrapper, baseClient, request, legacyAuditLogger } = setup({
|
||||
securityEnabled: true,
|
||||
});
|
||||
|
||||
const privileges = scenario.expectedPrivilege(authorization);
|
||||
|
||||
const checkPrivileges = jest.fn().mockResolvedValue({
|
||||
username,
|
||||
privileges: {
|
||||
kibana: [
|
||||
...privileges
|
||||
.map((privilege) => [
|
||||
{ resource: savedObjects[0].id, privilege, authorized: true },
|
||||
{ resource: savedObjects[1].id, privilege, authorized: false },
|
||||
])
|
||||
.flat(),
|
||||
],
|
||||
},
|
||||
} as CheckPrivilegesResponse);
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({ atSpaces: checkPrivileges });
|
||||
|
||||
const actualSpaces = await wrapper.getAll({ purpose: scenario.purpose });
|
||||
|
||||
expect(actualSpaces).toEqual([spaces[0]]);
|
||||
expect(baseClient.getAll).toHaveBeenCalledWith({ purpose: scenario.purpose ?? 'any' });
|
||||
expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
expect(checkPrivileges).toHaveBeenCalledWith(
|
||||
savedObjects.map((savedObject) => savedObject.id),
|
||||
{ kibana: privileges }
|
||||
);
|
||||
|
||||
expectSuccessAuditLogging(legacyAuditLogger, username, 'getAll', [spaces[0].id]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get', () => {
|
||||
it('delegates to base client when security is not enabled', async () => {
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger } = setup({
|
||||
securityEnabled: false,
|
||||
});
|
||||
|
||||
const response = await wrapper.get('default');
|
||||
expect(baseClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(baseClient.get).toHaveBeenCalledWith('default');
|
||||
expect(response).toEqual(spaces[0]);
|
||||
expectNoAuthorizationCheck(authorization);
|
||||
expectNoAuditLogging(legacyAuditLogger);
|
||||
});
|
||||
|
||||
test(`throws a forbidden error when unauthorized`, async () => {
|
||||
const username = 'some_user';
|
||||
const spaceId = 'default';
|
||||
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({
|
||||
securityEnabled: true,
|
||||
});
|
||||
|
||||
const checkPrivileges = jest.fn().mockResolvedValue({
|
||||
username,
|
||||
hasAllRequested: false,
|
||||
privileges: {
|
||||
kibana: [
|
||||
{ resource: spaceId, privilege: authorization.actions.login, authorized: false },
|
||||
],
|
||||
},
|
||||
} as CheckPrivilegesResponse);
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({ atSpace: checkPrivileges });
|
||||
|
||||
await expect(wrapper.get(spaceId)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unauthorized to get default space"`
|
||||
);
|
||||
|
||||
expect(baseClient.get).not.toHaveBeenCalled();
|
||||
|
||||
expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
expect(checkPrivileges).toHaveBeenCalledWith(spaceId, {
|
||||
kibana: authorization.actions.login,
|
||||
});
|
||||
|
||||
expectForbiddenAuditLogging(legacyAuditLogger, username, 'get', spaceId);
|
||||
});
|
||||
|
||||
it('returns the space when authorized', async () => {
|
||||
const username = 'some_user';
|
||||
const spaceId = 'default';
|
||||
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({
|
||||
securityEnabled: true,
|
||||
});
|
||||
|
||||
const checkPrivileges = jest.fn().mockResolvedValue({
|
||||
username,
|
||||
hasAllRequested: true,
|
||||
privileges: {
|
||||
kibana: [{ resource: spaceId, privilege: authorization.actions.login, authorized: true }],
|
||||
},
|
||||
} as CheckPrivilegesResponse);
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({ atSpace: checkPrivileges });
|
||||
|
||||
const response = await wrapper.get(spaceId);
|
||||
|
||||
expect(baseClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(baseClient.get).toHaveBeenCalledWith(spaceId);
|
||||
|
||||
expect(response).toEqual(spaces[0]);
|
||||
|
||||
expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
expect(checkPrivileges).toHaveBeenCalledWith(spaceId, {
|
||||
kibana: authorization.actions.login,
|
||||
});
|
||||
|
||||
expectSuccessAuditLogging(legacyAuditLogger, username, 'get', [spaceId]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#create', () => {
|
||||
const space = Object.freeze({
|
||||
id: 'new_space',
|
||||
name: 'new space',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
|
||||
it('delegates to base client when security is not enabled', async () => {
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger } = setup({
|
||||
securityEnabled: false,
|
||||
});
|
||||
|
||||
const response = await wrapper.create(space);
|
||||
expect(baseClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(baseClient.create).toHaveBeenCalledWith(space);
|
||||
expect(response).toEqual(space);
|
||||
expectNoAuthorizationCheck(authorization);
|
||||
expectNoAuditLogging(legacyAuditLogger);
|
||||
});
|
||||
|
||||
test(`throws a forbidden error when unauthorized`, async () => {
|
||||
const username = 'some_user';
|
||||
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({
|
||||
securityEnabled: true,
|
||||
});
|
||||
|
||||
const checkPrivileges = jest.fn().mockResolvedValue({
|
||||
username,
|
||||
hasAllRequested: false,
|
||||
privileges: {
|
||||
kibana: [{ privilege: authorization.actions.space.manage, authorized: false }],
|
||||
},
|
||||
} as CheckPrivilegesResponse);
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges });
|
||||
|
||||
await expect(wrapper.create(space)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unauthorized to create spaces"`
|
||||
);
|
||||
|
||||
expect(baseClient.create).not.toHaveBeenCalled();
|
||||
|
||||
expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: authorization.actions.space.manage,
|
||||
});
|
||||
|
||||
expectForbiddenAuditLogging(legacyAuditLogger, username, 'create');
|
||||
});
|
||||
|
||||
it('creates the space when authorized', async () => {
|
||||
const username = 'some_user';
|
||||
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({
|
||||
securityEnabled: true,
|
||||
});
|
||||
|
||||
const checkPrivileges = jest.fn().mockResolvedValue({
|
||||
username,
|
||||
hasAllRequested: true,
|
||||
privileges: {
|
||||
kibana: [{ privilege: authorization.actions.space.manage, authorized: true }],
|
||||
},
|
||||
} as CheckPrivilegesResponse);
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges });
|
||||
|
||||
const response = await wrapper.create(space);
|
||||
|
||||
expect(baseClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(baseClient.create).toHaveBeenCalledWith(space);
|
||||
|
||||
expect(response).toEqual(space);
|
||||
|
||||
expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: authorization.actions.space.manage,
|
||||
});
|
||||
|
||||
expectSuccessAuditLogging(legacyAuditLogger, username, 'create');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
const space = Object.freeze({
|
||||
id: 'existing_space',
|
||||
name: 'existing space',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
|
||||
it('delegates to base client when security is not enabled', async () => {
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger } = setup({
|
||||
securityEnabled: false,
|
||||
});
|
||||
|
||||
const response = await wrapper.update(space.id, space);
|
||||
expect(baseClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(baseClient.update).toHaveBeenCalledWith(space.id, space);
|
||||
expect(response).toEqual(space.id);
|
||||
expectNoAuthorizationCheck(authorization);
|
||||
expectNoAuditLogging(legacyAuditLogger);
|
||||
});
|
||||
|
||||
test(`throws a forbidden error when unauthorized`, async () => {
|
||||
const username = 'some_user';
|
||||
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({
|
||||
securityEnabled: true,
|
||||
});
|
||||
|
||||
const checkPrivileges = jest.fn().mockResolvedValue({
|
||||
username,
|
||||
hasAllRequested: false,
|
||||
privileges: {
|
||||
kibana: [{ privilege: authorization.actions.space.manage, authorized: false }],
|
||||
},
|
||||
} as CheckPrivilegesResponse);
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges });
|
||||
|
||||
await expect(wrapper.update(space.id, space)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unauthorized to update spaces"`
|
||||
);
|
||||
|
||||
expect(baseClient.update).not.toHaveBeenCalled();
|
||||
|
||||
expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: authorization.actions.space.manage,
|
||||
});
|
||||
|
||||
expectForbiddenAuditLogging(legacyAuditLogger, username, 'update');
|
||||
});
|
||||
|
||||
it('updates the space when authorized', async () => {
|
||||
const username = 'some_user';
|
||||
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({
|
||||
securityEnabled: true,
|
||||
});
|
||||
|
||||
const checkPrivileges = jest.fn().mockResolvedValue({
|
||||
username,
|
||||
hasAllRequested: true,
|
||||
privileges: {
|
||||
kibana: [{ privilege: authorization.actions.space.manage, authorized: true }],
|
||||
},
|
||||
} as CheckPrivilegesResponse);
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges });
|
||||
|
||||
const response = await wrapper.update(space.id, space);
|
||||
|
||||
expect(baseClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(baseClient.update).toHaveBeenCalledWith(space.id, space);
|
||||
|
||||
expect(response).toEqual(space.id);
|
||||
|
||||
expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: authorization.actions.space.manage,
|
||||
});
|
||||
|
||||
expectSuccessAuditLogging(legacyAuditLogger, username, 'update');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
const space = Object.freeze({
|
||||
id: 'existing_space',
|
||||
name: 'existing space',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
|
||||
it('delegates to base client when security is not enabled', async () => {
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger } = setup({
|
||||
securityEnabled: false,
|
||||
});
|
||||
|
||||
await wrapper.delete(space.id);
|
||||
expect(baseClient.delete).toHaveBeenCalledTimes(1);
|
||||
expect(baseClient.delete).toHaveBeenCalledWith(space.id);
|
||||
expectNoAuthorizationCheck(authorization);
|
||||
expectNoAuditLogging(legacyAuditLogger);
|
||||
});
|
||||
|
||||
test(`throws a forbidden error when unauthorized`, async () => {
|
||||
const username = 'some_user';
|
||||
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({
|
||||
securityEnabled: true,
|
||||
});
|
||||
|
||||
const checkPrivileges = jest.fn().mockResolvedValue({
|
||||
username,
|
||||
hasAllRequested: false,
|
||||
privileges: {
|
||||
kibana: [{ privilege: authorization.actions.space.manage, authorized: false }],
|
||||
},
|
||||
} as CheckPrivilegesResponse);
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges });
|
||||
|
||||
await expect(wrapper.delete(space.id)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unauthorized to delete spaces"`
|
||||
);
|
||||
|
||||
expect(baseClient.delete).not.toHaveBeenCalled();
|
||||
|
||||
expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: authorization.actions.space.manage,
|
||||
});
|
||||
|
||||
expectForbiddenAuditLogging(legacyAuditLogger, username, 'delete');
|
||||
});
|
||||
|
||||
it('deletes the space when authorized', async () => {
|
||||
const username = 'some_user';
|
||||
|
||||
const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({
|
||||
securityEnabled: true,
|
||||
});
|
||||
|
||||
const checkPrivileges = jest.fn().mockResolvedValue({
|
||||
username,
|
||||
hasAllRequested: true,
|
||||
privileges: {
|
||||
kibana: [{ privilege: authorization.actions.space.manage, authorized: true }],
|
||||
},
|
||||
} as CheckPrivilegesResponse);
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges });
|
||||
|
||||
await wrapper.delete(space.id);
|
||||
|
||||
expect(baseClient.delete).toHaveBeenCalledTimes(1);
|
||||
expect(baseClient.delete).toHaveBeenCalledWith(space.id);
|
||||
|
||||
expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: authorization.actions.space.manage,
|
||||
});
|
||||
|
||||
expectSuccessAuditLogging(legacyAuditLogger, username, 'delete');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { GetAllSpacesPurpose, GetSpaceResult } from '../../../spaces/common/model/types';
|
||||
import { Space, ISpacesClient } from '../../../spaces/server';
|
||||
import { LegacySpacesAuditLogger } from './legacy_audit_logger';
|
||||
import { AuthorizationServiceSetup } from '../authorization';
|
||||
import { SecurityPluginSetup } from '..';
|
||||
|
||||
const PURPOSE_PRIVILEGE_MAP: Record<
|
||||
GetAllSpacesPurpose,
|
||||
(authorization: SecurityPluginSetup['authz']) => string[]
|
||||
> = {
|
||||
any: (authorization) => [authorization.actions.login],
|
||||
copySavedObjectsIntoSpace: (authorization) => [
|
||||
authorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'),
|
||||
],
|
||||
findSavedObjects: (authorization) => {
|
||||
return [authorization.actions.login, authorization.actions.savedObject.get('config', 'find')];
|
||||
},
|
||||
shareSavedObjectsIntoSpace: (authorization) => [
|
||||
authorization.actions.ui.get('savedObjectsManagement', 'shareIntoSpace'),
|
||||
],
|
||||
};
|
||||
|
||||
interface GetAllSpacesOptions {
|
||||
purpose?: GetAllSpacesPurpose;
|
||||
includeAuthorizedPurposes?: boolean;
|
||||
}
|
||||
|
||||
export class SecureSpacesClientWrapper implements ISpacesClient {
|
||||
private readonly useRbac = this.authorization.mode.useRbacForRequest(this.request);
|
||||
|
||||
constructor(
|
||||
private readonly spacesClient: ISpacesClient,
|
||||
private readonly request: KibanaRequest,
|
||||
private readonly authorization: AuthorizationServiceSetup,
|
||||
private readonly legacyAuditLogger: LegacySpacesAuditLogger
|
||||
) {}
|
||||
|
||||
public async getAll({
|
||||
purpose = 'any',
|
||||
includeAuthorizedPurposes,
|
||||
}: GetAllSpacesOptions = {}): Promise<GetSpaceResult[]> {
|
||||
const allSpaces = await this.spacesClient.getAll({ purpose, includeAuthorizedPurposes });
|
||||
|
||||
if (!this.useRbac) {
|
||||
return allSpaces;
|
||||
}
|
||||
|
||||
const spaceIds = allSpaces.map((space: Space) => space.id);
|
||||
|
||||
const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request);
|
||||
|
||||
// Collect all privileges which need to be checked
|
||||
const allPrivileges = Object.entries(PURPOSE_PRIVILEGE_MAP).reduce(
|
||||
(acc, [getSpacesPurpose, privilegeFactory]) =>
|
||||
!includeAuthorizedPurposes && getSpacesPurpose !== purpose
|
||||
? acc
|
||||
: { ...acc, [getSpacesPurpose]: privilegeFactory(this.authorization) },
|
||||
{} as Record<GetAllSpacesPurpose, string[]>
|
||||
);
|
||||
|
||||
// Check all privileges against all spaces
|
||||
const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, {
|
||||
kibana: Object.values(allPrivileges).flat(),
|
||||
});
|
||||
|
||||
// Determine which purposes the user is authorized for within each space.
|
||||
// Remove any spaces for which user is fully unauthorized.
|
||||
const checkHasAllRequired = (space: Space, actions: string[]) =>
|
||||
actions.every((action) =>
|
||||
privileges.kibana.some(
|
||||
({ resource, privilege, authorized }) =>
|
||||
resource === space.id && privilege === action && authorized
|
||||
)
|
||||
);
|
||||
const authorizedSpaces: GetSpaceResult[] = allSpaces
|
||||
.map((space: Space) => {
|
||||
if (!includeAuthorizedPurposes) {
|
||||
// Check if the user is authorized for a single purpose
|
||||
const requiredActions = PURPOSE_PRIVILEGE_MAP[purpose](this.authorization);
|
||||
return checkHasAllRequired(space, requiredActions) ? space : null;
|
||||
}
|
||||
|
||||
// Check if the user is authorized for each purpose
|
||||
let hasAnyAuthorization = false;
|
||||
const authorizedPurposes = Object.entries(PURPOSE_PRIVILEGE_MAP).reduce(
|
||||
(acc, [purposeKey, privilegeFactory]) => {
|
||||
const requiredActions = privilegeFactory(this.authorization);
|
||||
const hasAllRequired = checkHasAllRequired(space, requiredActions);
|
||||
hasAnyAuthorization = hasAnyAuthorization || hasAllRequired;
|
||||
return { ...acc, [purposeKey]: hasAllRequired };
|
||||
},
|
||||
{} as Record<GetAllSpacesPurpose, boolean>
|
||||
);
|
||||
|
||||
if (!hasAnyAuthorization) {
|
||||
return null;
|
||||
}
|
||||
return { ...space, authorizedPurposes };
|
||||
})
|
||||
.filter(this.filterUnauthorizedSpaceResults);
|
||||
|
||||
if (authorizedSpaces.length === 0) {
|
||||
this.legacyAuditLogger.spacesAuthorizationFailure(username, 'getAll');
|
||||
throw Boom.forbidden(); // Note: there is a catch for this in `SpacesSavedObjectsClient.find`; if we get rid of this error, remove that too
|
||||
}
|
||||
|
||||
const authorizedSpaceIds = authorizedSpaces.map((space) => space.id);
|
||||
this.legacyAuditLogger.spacesAuthorizationSuccess(username, 'getAll', authorizedSpaceIds);
|
||||
|
||||
return authorizedSpaces;
|
||||
}
|
||||
|
||||
public async get(id: string) {
|
||||
if (this.useRbac) {
|
||||
await this.ensureAuthorizedAtSpace(
|
||||
id,
|
||||
this.authorization.actions.login,
|
||||
'get',
|
||||
`Unauthorized to get ${id} space`
|
||||
);
|
||||
}
|
||||
|
||||
return this.spacesClient.get(id);
|
||||
}
|
||||
|
||||
public async create(space: Space) {
|
||||
if (this.useRbac) {
|
||||
await this.ensureAuthorizedGlobally(
|
||||
this.authorization.actions.space.manage,
|
||||
'create',
|
||||
'Unauthorized to create spaces'
|
||||
);
|
||||
}
|
||||
|
||||
return this.spacesClient.create(space);
|
||||
}
|
||||
|
||||
public async update(id: string, space: Space) {
|
||||
if (this.useRbac) {
|
||||
await this.ensureAuthorizedGlobally(
|
||||
this.authorization.actions.space.manage,
|
||||
'update',
|
||||
'Unauthorized to update spaces'
|
||||
);
|
||||
}
|
||||
|
||||
return this.spacesClient.update(id, space);
|
||||
}
|
||||
|
||||
public async delete(id: string) {
|
||||
if (this.useRbac) {
|
||||
await this.ensureAuthorizedGlobally(
|
||||
this.authorization.actions.space.manage,
|
||||
'delete',
|
||||
'Unauthorized to delete spaces'
|
||||
);
|
||||
}
|
||||
|
||||
return this.spacesClient.delete(id);
|
||||
}
|
||||
|
||||
private async ensureAuthorizedGlobally(action: string, method: string, forbiddenMessage: string) {
|
||||
const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request);
|
||||
const { username, hasAllRequested } = await checkPrivileges.globally({ kibana: action });
|
||||
|
||||
if (hasAllRequested) {
|
||||
this.legacyAuditLogger.spacesAuthorizationSuccess(username, method);
|
||||
} else {
|
||||
this.legacyAuditLogger.spacesAuthorizationFailure(username, method);
|
||||
throw Boom.forbidden(forbiddenMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureAuthorizedAtSpace(
|
||||
spaceId: string,
|
||||
action: string,
|
||||
method: string,
|
||||
forbiddenMessage: string
|
||||
) {
|
||||
const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request);
|
||||
const { username, hasAllRequested } = await checkPrivileges.atSpace(spaceId, {
|
||||
kibana: action,
|
||||
});
|
||||
|
||||
if (hasAllRequested) {
|
||||
this.legacyAuditLogger.spacesAuthorizationSuccess(username, method, [spaceId]);
|
||||
} else {
|
||||
this.legacyAuditLogger.spacesAuthorizationFailure(username, method, [spaceId]);
|
||||
throw Boom.forbidden(forbiddenMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private filterUnauthorizedSpaceResults(value: GetSpaceResult | null): value is GetSpaceResult {
|
||||
return value !== null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { coreMock, httpServerMock } from '../../../../../src/core/server/mocks';
|
||||
|
||||
import { spacesMock } from '../../../spaces/server/mocks';
|
||||
|
||||
import { auditServiceMock } from '../audit/index.mock';
|
||||
import { authorizationMock } from '../authorization/index.mock';
|
||||
import { setupSpacesClient } from './setup_spaces_client';
|
||||
|
||||
describe('setupSpacesClient', () => {
|
||||
it('does not setup the spaces client when spaces is disabled', () => {
|
||||
const authz = authorizationMock.create();
|
||||
const audit = auditServiceMock.create();
|
||||
|
||||
setupSpacesClient({ authz, audit });
|
||||
|
||||
expect(audit.getLogger).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('configures the repository factory, wrapper, and audit logger', () => {
|
||||
const authz = authorizationMock.create();
|
||||
const audit = auditServiceMock.create();
|
||||
const spaces = spacesMock.createSetup();
|
||||
|
||||
setupSpacesClient({ authz, audit, spaces });
|
||||
|
||||
expect(spaces.spacesClient.registerClientWrapper).toHaveBeenCalledTimes(1);
|
||||
expect(spaces.spacesClient.setClientRepositoryFactory).toHaveBeenCalledTimes(1);
|
||||
expect(audit.getLogger).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('creates a factory that creates an internal repository when RBAC is used for the request', () => {
|
||||
const authz = authorizationMock.create();
|
||||
const audit = auditServiceMock.create();
|
||||
const spaces = spacesMock.createSetup();
|
||||
|
||||
const { savedObjects } = coreMock.createStart();
|
||||
|
||||
setupSpacesClient({ authz, audit, spaces });
|
||||
|
||||
expect(spaces.spacesClient.setClientRepositoryFactory).toHaveBeenCalledTimes(1);
|
||||
const [repositoryFactory] = spaces.spacesClient.setClientRepositoryFactory.mock.calls[0];
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
authz.mode.useRbacForRequest.mockReturnValueOnce(true);
|
||||
|
||||
repositoryFactory(request, savedObjects);
|
||||
|
||||
expect(savedObjects.createInternalRepository).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjects.createInternalRepository).toHaveBeenCalledWith(['space']);
|
||||
expect(savedObjects.createScopedRepository).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a factory that creates a scoped repository when RBAC is NOT used for the request', () => {
|
||||
const authz = authorizationMock.create();
|
||||
const audit = auditServiceMock.create();
|
||||
const spaces = spacesMock.createSetup();
|
||||
|
||||
const { savedObjects } = coreMock.createStart();
|
||||
|
||||
setupSpacesClient({ authz, audit, spaces });
|
||||
|
||||
expect(spaces.spacesClient.setClientRepositoryFactory).toHaveBeenCalledTimes(1);
|
||||
const [repositoryFactory] = spaces.spacesClient.setClientRepositoryFactory.mock.calls[0];
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
authz.mode.useRbacForRequest.mockReturnValueOnce(false);
|
||||
|
||||
repositoryFactory(request, savedObjects);
|
||||
|
||||
expect(savedObjects.createInternalRepository).not.toHaveBeenCalled();
|
||||
expect(savedObjects.createScopedRepository).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjects.createScopedRepository).toHaveBeenCalledWith(request, ['space']);
|
||||
});
|
||||
});
|
38
x-pack/plugins/security/server/spaces/setup_spaces_client.ts
Normal file
38
x-pack/plugins/security/server/spaces/setup_spaces_client.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { SpacesPluginSetup } from '../../../spaces/server';
|
||||
import { AuditServiceSetup } from '../audit';
|
||||
import { AuthorizationServiceSetup } from '../authorization';
|
||||
import { LegacySpacesAuditLogger } from './legacy_audit_logger';
|
||||
import { SecureSpacesClientWrapper } from './secure_spaces_client_wrapper';
|
||||
|
||||
interface Deps {
|
||||
audit: AuditServiceSetup;
|
||||
authz: AuthorizationServiceSetup;
|
||||
spaces?: SpacesPluginSetup;
|
||||
}
|
||||
|
||||
export const setupSpacesClient = ({ audit, authz, spaces }: Deps) => {
|
||||
if (!spaces) {
|
||||
return;
|
||||
}
|
||||
const { spacesClient } = spaces;
|
||||
|
||||
spacesClient.setClientRepositoryFactory((request, savedObjectsStart) => {
|
||||
if (authz.mode.useRbacForRequest(request)) {
|
||||
return savedObjectsStart.createInternalRepository(['space']);
|
||||
}
|
||||
return savedObjectsStart.createScopedRepository(request, ['space']);
|
||||
});
|
||||
|
||||
const spacesAuditLogger = new LegacySpacesAuditLogger(audit.getLogger());
|
||||
|
||||
spacesClient.registerClientWrapper(
|
||||
(request, baseClient) =>
|
||||
new SecureSpacesClientWrapper(baseClient, request, authz, spacesAuditLogger)
|
||||
);
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`addSpaceIdToPath it throws an error when the requested path does not start with a slash 1`] = `"path must start with a /"`;
|
|
@ -102,6 +102,6 @@ describe('addSpaceIdToPath', () => {
|
|||
test('it throws an error when the requested path does not start with a slash', () => {
|
||||
expect(() => {
|
||||
addSpaceIdToPath('', '', 'foo');
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"path must start with a /"`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -47,10 +47,12 @@ export function addSpaceIdToPath(
|
|||
throw new Error(`path must start with a /`);
|
||||
}
|
||||
|
||||
const normalizedBasePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
||||
|
||||
if (spaceId && spaceId !== DEFAULT_SPACE_ID) {
|
||||
return `${basePath}/s/${spaceId}${requestedPath}`;
|
||||
return `${normalizedBasePath}/s/${spaceId}${requestedPath}`;
|
||||
}
|
||||
return `${basePath}${requestedPath}`;
|
||||
return `${normalizedBasePath}${requestedPath}` || '/';
|
||||
}
|
||||
|
||||
function stripServerBasePath(requestBasePath: string, serverBasePath: string) {
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
"advancedSettings",
|
||||
"home",
|
||||
"management",
|
||||
"security",
|
||||
"usageCollection",
|
||||
"savedObjectsManagement"
|
||||
],
|
||||
|
|
|
@ -116,12 +116,54 @@ describe('SpacesManager', () => {
|
|||
const result = await spacesManager.getShareSavedObjectPermissions('foo');
|
||||
expect(coreStart.http.get).toHaveBeenCalledTimes(2);
|
||||
expect(coreStart.http.get).toHaveBeenLastCalledWith(
|
||||
'/internal/spaces/_share_saved_object_permissions',
|
||||
'/internal/security/_share_saved_object_permissions',
|
||||
{
|
||||
query: { type: 'foo' },
|
||||
}
|
||||
);
|
||||
expect(result).toEqual({ shareToAllSpaces });
|
||||
});
|
||||
|
||||
it('allows the share if security is disabled', async () => {
|
||||
const coreStart = coreMock.createStart();
|
||||
coreStart.http.get.mockResolvedValueOnce({});
|
||||
coreStart.http.get.mockRejectedValueOnce({
|
||||
body: {
|
||||
statusCode: 404,
|
||||
},
|
||||
});
|
||||
const spacesManager = new SpacesManager(coreStart.http);
|
||||
expect(coreStart.http.get).toHaveBeenCalledTimes(1); // initial call to get active space
|
||||
|
||||
const result = await spacesManager.getShareSavedObjectPermissions('foo');
|
||||
expect(coreStart.http.get).toHaveBeenCalledTimes(2);
|
||||
expect(coreStart.http.get).toHaveBeenLastCalledWith(
|
||||
'/internal/security/_share_saved_object_permissions',
|
||||
{
|
||||
query: { type: 'foo' },
|
||||
}
|
||||
);
|
||||
expect(result).toEqual({ shareToAllSpaces: true });
|
||||
});
|
||||
|
||||
it('throws all other errors', async () => {
|
||||
const coreStart = coreMock.createStart();
|
||||
coreStart.http.get.mockResolvedValueOnce({});
|
||||
coreStart.http.get.mockRejectedValueOnce(new Error('Get out of here!'));
|
||||
const spacesManager = new SpacesManager(coreStart.http);
|
||||
expect(coreStart.http.get).toHaveBeenCalledTimes(1); // initial call to get active space
|
||||
|
||||
await expect(
|
||||
spacesManager.getShareSavedObjectPermissions('foo')
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Get out of here!"`);
|
||||
|
||||
expect(coreStart.http.get).toHaveBeenCalledTimes(2);
|
||||
expect(coreStart.http.get).toHaveBeenLastCalledWith(
|
||||
'/internal/security/_share_saved_object_permissions',
|
||||
{
|
||||
query: { type: 'foo' },
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -115,7 +115,16 @@ export class SpacesManager {
|
|||
public async getShareSavedObjectPermissions(
|
||||
type: string
|
||||
): Promise<{ shareToAllSpaces: boolean }> {
|
||||
return this.http.get('/internal/spaces/_share_saved_object_permissions', { query: { type } });
|
||||
return this.http
|
||||
.get('/internal/security/_share_saved_object_permissions', { query: { type } })
|
||||
.catch((err) => {
|
||||
const isNotFound = err?.body?.statusCode === 404;
|
||||
if (isNotFound) {
|
||||
// security is not enabled
|
||||
return { shareToAllSpaces: true };
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
public async shareSavedObjectAdd(object: SavedObject, spaces: string[]): Promise<void> {
|
||||
|
|
|
@ -126,14 +126,14 @@ const setup = (space: Space) => {
|
|||
{},
|
||||
]);
|
||||
|
||||
const spacesService = spacesServiceMock.createSetupContract();
|
||||
const spacesService = spacesServiceMock.createStartContract();
|
||||
spacesService.getActiveSpace.mockResolvedValue(space);
|
||||
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
const switcher = setupCapabilitiesSwitcher(
|
||||
(coreSetup as unknown) as CoreSetup<PluginsStart>,
|
||||
spacesService,
|
||||
() => spacesService,
|
||||
logger
|
||||
);
|
||||
|
||||
|
|
|
@ -7,12 +7,12 @@ import _ from 'lodash';
|
|||
import { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server';
|
||||
import { KibanaFeature } from '../../../../plugins/features/server';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { SpacesServiceSetup } from '../spaces_service';
|
||||
import { SpacesServiceStart } from '../spaces_service';
|
||||
import { PluginsStart } from '../plugin';
|
||||
|
||||
export function setupCapabilitiesSwitcher(
|
||||
core: CoreSetup<PluginsStart>,
|
||||
spacesService: SpacesServiceSetup,
|
||||
getSpacesService: () => SpacesServiceStart,
|
||||
logger: Logger
|
||||
): CapabilitiesSwitcher {
|
||||
return async (request, capabilities) => {
|
||||
|
@ -24,7 +24,7 @@ export function setupCapabilitiesSwitcher(
|
|||
|
||||
try {
|
||||
const [activeSpace, [, { features }]] = await Promise.all([
|
||||
spacesService.getActiveSpace(request),
|
||||
getSpacesService().getActiveSpace(request),
|
||||
core.getStartServices(),
|
||||
]);
|
||||
|
||||
|
|
|
@ -8,13 +8,13 @@ import { CoreSetup, Logger } from 'src/core/server';
|
|||
import { capabilitiesProvider } from './capabilities_provider';
|
||||
import { setupCapabilitiesSwitcher } from './capabilities_switcher';
|
||||
import { PluginsStart } from '../plugin';
|
||||
import { SpacesServiceSetup } from '../spaces_service';
|
||||
import { SpacesServiceStart } from '../spaces_service';
|
||||
|
||||
export const setupCapabilities = (
|
||||
core: CoreSetup<PluginsStart>,
|
||||
spacesService: SpacesServiceSetup,
|
||||
getSpacesService: () => SpacesServiceStart,
|
||||
logger: Logger
|
||||
) => {
|
||||
core.capabilities.registerProvider(capabilitiesProvider);
|
||||
core.capabilities.registerSwitcher(setupCapabilitiesSwitcher(core, spacesService, logger));
|
||||
core.capabilities.registerSwitcher(setupCapabilitiesSwitcher(core, getSpacesService, logger));
|
||||
};
|
||||
|
|
|
@ -13,10 +13,13 @@ import { Plugin } from './plugin';
|
|||
// reduce number of such exports to zero and provide everything we want to expose via Setup/Start
|
||||
// run-time contracts.
|
||||
|
||||
export { addSpaceIdToPath } from '../common';
|
||||
|
||||
// end public contract exports
|
||||
|
||||
export { SpacesPluginSetup } from './plugin';
|
||||
export { SpacesServiceSetup } from './spaces_service';
|
||||
export { SpacesPluginSetup, SpacesPluginStart } from './plugin';
|
||||
export { SpacesServiceSetup, SpacesServiceStart } from './spaces_service';
|
||||
export { ISpacesClient } from './spaces_client';
|
||||
export { Space } from '../common/model/space';
|
||||
|
||||
export const config = { schema: ConfigSchema };
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as Rx from 'rxjs';
|
||||
import Boom from '@hapi/boom';
|
||||
import { Legacy } from 'kibana';
|
||||
// @ts-ignore
|
||||
|
@ -22,13 +21,11 @@ import {
|
|||
} from '../../../../../../src/core/server/mocks';
|
||||
import * as kbnTestServer from '../../../../../../src/core/test_helpers/kbn_server';
|
||||
import { SpacesService } from '../../spaces_service';
|
||||
import { SpacesAuditLogger } from '../audit_logger';
|
||||
import { convertSavedObjectToSpace } from '../../routes/lib';
|
||||
import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor';
|
||||
import { KibanaFeature } from '../../../../features/server';
|
||||
import { spacesConfig } from '../__fixtures__';
|
||||
import { securityMock } from '../../../../security/server/mocks';
|
||||
import { featuresPluginMock } from '../../../../features/server/mocks';
|
||||
import { spacesClientServiceMock } from '../../spaces_client/spaces_client_service.mock';
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/55953
|
||||
describe.skip('onPostAuthInterceptor', () => {
|
||||
|
@ -166,17 +163,18 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
coreStart.savedObjects.createInternalRepository.mockImplementation(mockRepository);
|
||||
coreStart.savedObjects.createScopedRepository.mockImplementation(mockRepository);
|
||||
|
||||
const service = new SpacesService(loggingMock);
|
||||
const service = new SpacesService();
|
||||
|
||||
const spacesService = await service.setup({
|
||||
http: (http as unknown) as CoreSetup['http'],
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
authorization: securityMock.createSetup().authz,
|
||||
auditLogger: {} as SpacesAuditLogger,
|
||||
config$: Rx.of(spacesConfig),
|
||||
service.setup({
|
||||
basePath: http.basePath,
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn().mockResolvedValue({
|
||||
const spacesServiceStart = service.start({
|
||||
basePath: http.basePath,
|
||||
spacesClientService: spacesClientServiceMock.createStart(),
|
||||
});
|
||||
|
||||
spacesServiceStart.createSpacesClient = jest.fn().mockReturnValue({
|
||||
getAll() {
|
||||
if (testOptions.simulateGetSpacesFailure) {
|
||||
throw Boom.unauthorized('missing credendials', 'Protected Elasticsearch');
|
||||
|
@ -206,7 +204,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
http: (http as unknown) as CoreSetup['http'],
|
||||
log: loggingMock,
|
||||
features: featuresPlugin,
|
||||
spacesService,
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
});
|
||||
|
||||
const router = http.createRouter('/');
|
||||
|
@ -221,7 +219,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
|
||||
return {
|
||||
response,
|
||||
spacesService,
|
||||
spacesService: spacesServiceStart,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -342,7 +340,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
}
|
||||
`);
|
||||
|
||||
expect(spacesService.scopedClient).toHaveBeenCalledWith(
|
||||
expect(spacesService.createSpacesClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: headers.authorization,
|
||||
|
@ -381,7 +379,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
}
|
||||
`);
|
||||
|
||||
expect(spacesService.scopedClient).toHaveBeenCalledWith(
|
||||
expect(spacesService.createSpacesClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: headers.authorization,
|
||||
|
@ -414,7 +412,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
expect(response.status).toEqual(302);
|
||||
expect(response.header.location).toEqual(`/spaces/space_selector`);
|
||||
|
||||
expect(spacesService.scopedClient).toHaveBeenCalledWith(
|
||||
expect(spacesService.createSpacesClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: headers.authorization,
|
||||
|
@ -447,7 +445,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
expect(response.status).toEqual(302);
|
||||
expect(response.header.location).toEqual(`/s/a-space/spaces/enter`);
|
||||
|
||||
expect(spacesService.scopedClient).toHaveBeenCalledWith(
|
||||
expect(spacesService.createSpacesClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: headers.authorization,
|
||||
|
@ -473,7 +471,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
expect(response.status).toEqual(302);
|
||||
expect(response.header.location).toEqual(`/s/a-space/spaces/enter`);
|
||||
|
||||
expect(spacesService.scopedClient).toHaveBeenCalledWith(
|
||||
expect(spacesService.createSpacesClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: headers.authorization,
|
||||
|
@ -501,7 +499,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.header.location).toEqual('/spaces/enter');
|
||||
expect(spacesService.scopedClient).toHaveBeenCalledWith(
|
||||
expect(spacesService.createSpacesClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: headers.authorization,
|
||||
|
@ -526,7 +524,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
expect(spacesService.scopedClient).toHaveBeenCalledWith(
|
||||
expect(spacesService.createSpacesClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: headers.authorization,
|
||||
|
@ -551,7 +549,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
expect(spacesService.scopedClient).toHaveBeenCalledWith(
|
||||
expect(spacesService.createSpacesClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: headers.authorization,
|
||||
|
@ -576,7 +574,7 @@ describe.skip('onPostAuthInterceptor', () => {
|
|||
|
||||
expect(response.status).toEqual(404);
|
||||
|
||||
expect(spacesService.scopedClient).toHaveBeenCalledWith(
|
||||
expect(spacesService.createSpacesClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: headers.authorization,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { Logger, CoreSetup } from 'src/core/server';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { wrapError } from '../errors';
|
||||
import { SpacesServiceSetup } from '../../spaces_service/spaces_service';
|
||||
import { SpacesServiceStart } from '../../spaces_service/spaces_service';
|
||||
import { PluginsSetup } from '../../plugin';
|
||||
import { getSpaceSelectorUrl } from '../get_space_selector_url';
|
||||
import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants';
|
||||
|
@ -15,13 +15,13 @@ import { addSpaceIdToPath } from '../../../common';
|
|||
export interface OnPostAuthInterceptorDeps {
|
||||
http: CoreSetup['http'];
|
||||
features: PluginsSetup['features'];
|
||||
spacesService: SpacesServiceSetup;
|
||||
getSpacesService: () => SpacesServiceStart;
|
||||
log: Logger;
|
||||
}
|
||||
|
||||
export function initSpacesOnPostAuthRequestInterceptor({
|
||||
features,
|
||||
spacesService,
|
||||
getSpacesService,
|
||||
log,
|
||||
http,
|
||||
}: OnPostAuthInterceptorDeps) {
|
||||
|
@ -30,6 +30,8 @@ export function initSpacesOnPostAuthRequestInterceptor({
|
|||
|
||||
const path = request.url.pathname;
|
||||
|
||||
const spacesService = getSpacesService();
|
||||
|
||||
const spaceId = spacesService.getSpaceId(request);
|
||||
|
||||
// The root of kibana is also the root of the defaut space,
|
||||
|
@ -43,7 +45,7 @@ export function initSpacesOnPostAuthRequestInterceptor({
|
|||
// which is not available at the time of "onRequest".
|
||||
if (isRequestingKibanaRoot) {
|
||||
try {
|
||||
const spacesClient = await spacesService.scopedClient(request);
|
||||
const spacesClient = spacesService.createSpacesClient(request);
|
||||
const spaces = await spacesClient.getAll();
|
||||
|
||||
if (spaces.length === 1) {
|
||||
|
@ -76,7 +78,7 @@ export function initSpacesOnPostAuthRequestInterceptor({
|
|||
try {
|
||||
log.debug(`Verifying access to space "${spaceId}"`);
|
||||
|
||||
const spacesClient = await spacesService.scopedClient(request);
|
||||
const spacesClient = spacesService.createSpacesClient(request);
|
||||
space = await spacesClient.get(spaceId);
|
||||
} catch (error) {
|
||||
const wrappedError = wrapError(error);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,309 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import Boom from '@hapi/boom';
|
||||
import { omit } from 'lodash';
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { SecurityPluginSetup } 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 { GetAllSpacesPurpose, GetSpaceResult } from '../../../common/model/types';
|
||||
|
||||
interface GetAllSpacesOptions {
|
||||
purpose?: GetAllSpacesPurpose;
|
||||
includeAuthorizedPurposes?: boolean;
|
||||
}
|
||||
|
||||
const SUPPORTED_GET_SPACE_PURPOSES: GetAllSpacesPurpose[] = [
|
||||
'any',
|
||||
'copySavedObjectsIntoSpace',
|
||||
'findSavedObjects',
|
||||
'shareSavedObjectsIntoSpace',
|
||||
];
|
||||
const DEFAULT_PURPOSE = 'any';
|
||||
|
||||
const PURPOSE_PRIVILEGE_MAP: Record<
|
||||
GetAllSpacesPurpose,
|
||||
(authorization: SecurityPluginSetup['authz']) => string[]
|
||||
> = {
|
||||
any: (authorization) => [authorization.actions.login],
|
||||
copySavedObjectsIntoSpace: (authorization) => [
|
||||
authorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'),
|
||||
],
|
||||
findSavedObjects: (authorization) => [
|
||||
authorization.actions.login,
|
||||
authorization.actions.savedObject.get('config', 'find'),
|
||||
],
|
||||
shareSavedObjectsIntoSpace: (authorization) => [
|
||||
authorization.actions.ui.get('savedObjectsManagement', 'shareIntoSpace'),
|
||||
],
|
||||
};
|
||||
|
||||
function filterUnauthorizedSpaceResults(value: GetSpaceResult | null): value is GetSpaceResult {
|
||||
return value !== null;
|
||||
}
|
||||
|
||||
export class SpacesClient {
|
||||
constructor(
|
||||
private readonly auditLogger: SpacesAuditLogger,
|
||||
private readonly debugLogger: (message: string) => void,
|
||||
private readonly authorization: SecurityPluginSetup['authz'] | null,
|
||||
private readonly callWithRequestSavedObjectRepository: any,
|
||||
private readonly config: ConfigType,
|
||||
private readonly internalSavedObjectRepository: any,
|
||||
private readonly request: KibanaRequest
|
||||
) {}
|
||||
|
||||
public async getAll(options: GetAllSpacesOptions = {}): Promise<GetSpaceResult[]> {
|
||||
const { purpose = DEFAULT_PURPOSE, includeAuthorizedPurposes = false } = options;
|
||||
if (!SUPPORTED_GET_SPACE_PURPOSES.includes(purpose)) {
|
||||
throw Boom.badRequest(`unsupported space purpose: ${purpose}`);
|
||||
}
|
||||
|
||||
if (options.purpose && includeAuthorizedPurposes) {
|
||||
throw Boom.badRequest(`'purpose' cannot be supplied with 'includeAuthorizedPurposes'`);
|
||||
}
|
||||
|
||||
if (this.useRbac()) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { saved_objects } = await this.internalSavedObjectRepository.find({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: this.config.maxSpaces,
|
||||
sortField: 'name.keyword',
|
||||
});
|
||||
|
||||
this.debugLogger(`SpacesClient.getAll(), using RBAC. Found ${saved_objects.length} spaces`);
|
||||
|
||||
const spaces: GetSpaceResult[] = saved_objects.map(this.transformSavedObjectToSpace);
|
||||
const spaceIds = spaces.map((space: Space) => space.id);
|
||||
|
||||
const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request);
|
||||
|
||||
// Collect all privileges which need to be checked
|
||||
const allPrivileges = Object.entries(PURPOSE_PRIVILEGE_MAP).reduce(
|
||||
(acc, [getSpacesPurpose, privilegeFactory]) =>
|
||||
!includeAuthorizedPurposes && getSpacesPurpose !== purpose
|
||||
? acc
|
||||
: { ...acc, [getSpacesPurpose]: privilegeFactory(this.authorization!) },
|
||||
{} as Record<GetAllSpacesPurpose, string[]>
|
||||
);
|
||||
|
||||
// Check all privileges against all spaces
|
||||
const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, {
|
||||
kibana: Object.values(allPrivileges).flat(),
|
||||
});
|
||||
|
||||
// Determine which purposes the user is authorized for within each space.
|
||||
// Remove any spaces for which user is fully unauthorized.
|
||||
const checkHasAllRequired = (space: Space, actions: string[]) =>
|
||||
actions.every((action) =>
|
||||
privileges.kibana.some(
|
||||
({ resource, privilege, authorized }) =>
|
||||
resource === space.id && privilege === action && authorized
|
||||
)
|
||||
);
|
||||
const authorizedSpaces = spaces
|
||||
.map((space: Space) => {
|
||||
if (!includeAuthorizedPurposes) {
|
||||
// Check if the user is authorized for a single purpose
|
||||
const requiredActions = PURPOSE_PRIVILEGE_MAP[purpose](this.authorization!);
|
||||
return checkHasAllRequired(space, requiredActions) ? space : null;
|
||||
}
|
||||
|
||||
// Check if the user is authorized for each purpose
|
||||
let hasAnyAuthorization = false;
|
||||
const authorizedPurposes = Object.entries(PURPOSE_PRIVILEGE_MAP).reduce(
|
||||
(acc, [purposeKey, privilegeFactory]) => {
|
||||
const requiredActions = privilegeFactory(this.authorization!);
|
||||
const hasAllRequired = checkHasAllRequired(space, requiredActions);
|
||||
hasAnyAuthorization = hasAnyAuthorization || hasAllRequired;
|
||||
return { ...acc, [purposeKey]: hasAllRequired };
|
||||
},
|
||||
{} as Record<GetAllSpacesPurpose, boolean>
|
||||
);
|
||||
|
||||
if (!hasAnyAuthorization) {
|
||||
return null;
|
||||
}
|
||||
return { ...space, authorizedPurposes };
|
||||
})
|
||||
.filter(filterUnauthorizedSpaceResults);
|
||||
|
||||
if (authorizedSpaces.length === 0) {
|
||||
this.debugLogger(
|
||||
`SpacesClient.getAll(), using RBAC. returning 403/Forbidden. Not authorized for any spaces for ${purpose} purpose.`
|
||||
);
|
||||
this.auditLogger.spacesAuthorizationFailure(username, 'getAll');
|
||||
throw Boom.forbidden(); // Note: there is a catch for this in `SpacesSavedObjectsClient.find`; if we get rid of this error, remove that too
|
||||
}
|
||||
|
||||
const authorizedSpaceIds = authorizedSpaces.map((s) => s.id);
|
||||
this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorizedSpaceIds);
|
||||
this.debugLogger(
|
||||
`SpacesClient.getAll(), using RBAC. returning spaces: ${authorizedSpaceIds.join(',')}`
|
||||
);
|
||||
return authorizedSpaces;
|
||||
} else {
|
||||
this.debugLogger(`SpacesClient.getAll(), NOT USING RBAC. querying all spaces`);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { saved_objects } = await this.callWithRequestSavedObjectRepository.find({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: this.config.maxSpaces,
|
||||
sortField: 'name.keyword',
|
||||
});
|
||||
|
||||
this.debugLogger(
|
||||
`SpacesClient.getAll(), NOT USING RBAC. Found ${saved_objects.length} spaces.`
|
||||
);
|
||||
|
||||
return saved_objects.map(this.transformSavedObjectToSpace);
|
||||
}
|
||||
}
|
||||
|
||||
public async get(id: string): Promise<Space> {
|
||||
if (this.useRbac()) {
|
||||
await this.ensureAuthorizedAtSpace(
|
||||
id,
|
||||
this.authorization!.actions.login,
|
||||
'get',
|
||||
`Unauthorized to get ${id} space`
|
||||
);
|
||||
}
|
||||
const repository = this.useRbac()
|
||||
? this.internalSavedObjectRepository
|
||||
: this.callWithRequestSavedObjectRepository;
|
||||
|
||||
const savedObject = await repository.get('space', id);
|
||||
return this.transformSavedObjectToSpace(savedObject);
|
||||
}
|
||||
|
||||
public async create(space: Space) {
|
||||
if (this.useRbac()) {
|
||||
this.debugLogger(`SpacesClient.create(), using RBAC. Checking if authorized globally`);
|
||||
|
||||
await this.ensureAuthorizedGlobally(
|
||||
this.authorization!.actions.space.manage,
|
||||
'create',
|
||||
'Unauthorized to create spaces'
|
||||
);
|
||||
|
||||
this.debugLogger(`SpacesClient.create(), using RBAC. Global authorization check succeeded`);
|
||||
}
|
||||
const repository = this.useRbac()
|
||||
? this.internalSavedObjectRepository
|
||||
: this.callWithRequestSavedObjectRepository;
|
||||
|
||||
const { total } = await repository.find({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
});
|
||||
if (total >= this.config.maxSpaces) {
|
||||
throw Boom.badRequest(
|
||||
'Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting'
|
||||
);
|
||||
}
|
||||
|
||||
this.debugLogger(`SpacesClient.create(), using RBAC. Attempting to create space`);
|
||||
|
||||
const attributes = omit(space, ['id', '_reserved']);
|
||||
const id = space.id;
|
||||
const createdSavedObject = await repository.create('space', attributes, { id });
|
||||
|
||||
this.debugLogger(`SpacesClient.create(), created space object`);
|
||||
|
||||
return this.transformSavedObjectToSpace(createdSavedObject);
|
||||
}
|
||||
|
||||
public async update(id: string, space: Space) {
|
||||
if (this.useRbac()) {
|
||||
await this.ensureAuthorizedGlobally(
|
||||
this.authorization!.actions.space.manage,
|
||||
'update',
|
||||
'Unauthorized to update spaces'
|
||||
);
|
||||
}
|
||||
const repository = this.useRbac()
|
||||
? this.internalSavedObjectRepository
|
||||
: this.callWithRequestSavedObjectRepository;
|
||||
|
||||
const attributes = omit(space, 'id', '_reserved');
|
||||
await repository.update('space', id, attributes);
|
||||
const updatedSavedObject = await repository.get('space', id);
|
||||
return this.transformSavedObjectToSpace(updatedSavedObject);
|
||||
}
|
||||
|
||||
public async delete(id: string) {
|
||||
if (this.useRbac()) {
|
||||
await this.ensureAuthorizedGlobally(
|
||||
this.authorization!.actions.space.manage,
|
||||
'delete',
|
||||
'Unauthorized to delete spaces'
|
||||
);
|
||||
}
|
||||
|
||||
const repository = this.useRbac()
|
||||
? this.internalSavedObjectRepository
|
||||
: this.callWithRequestSavedObjectRepository;
|
||||
|
||||
const existingSavedObject = await repository.get('space', id);
|
||||
if (isReservedSpace(this.transformSavedObjectToSpace(existingSavedObject))) {
|
||||
throw Boom.badRequest('This Space cannot be deleted because it is reserved.');
|
||||
}
|
||||
|
||||
await repository.deleteByNamespace(id);
|
||||
|
||||
await repository.delete('space', id);
|
||||
}
|
||||
|
||||
private useRbac(): boolean {
|
||||
return this.authorization != null && this.authorization.mode.useRbacForRequest(this.request);
|
||||
}
|
||||
|
||||
private async ensureAuthorizedGlobally(action: string, method: string, forbiddenMessage: string) {
|
||||
const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request);
|
||||
const { username, hasAllRequested } = await checkPrivileges.globally({ kibana: action });
|
||||
|
||||
if (hasAllRequested) {
|
||||
this.auditLogger.spacesAuthorizationSuccess(username, method);
|
||||
return;
|
||||
} else {
|
||||
this.auditLogger.spacesAuthorizationFailure(username, method);
|
||||
throw Boom.forbidden(forbiddenMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureAuthorizedAtSpace(
|
||||
spaceId: string,
|
||||
action: string,
|
||||
method: string,
|
||||
forbiddenMessage: string
|
||||
) {
|
||||
const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request);
|
||||
const { username, hasAllRequested } = await checkPrivileges.atSpace(spaceId, {
|
||||
kibana: action,
|
||||
});
|
||||
|
||||
if (hasAllRequested) {
|
||||
this.auditLogger.spacesAuthorizationSuccess(username, method, [spaceId]);
|
||||
return;
|
||||
} else {
|
||||
this.auditLogger.spacesAuthorizationFailure(username, method, [spaceId]);
|
||||
throw Boom.forbidden(forbiddenMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private transformSavedObjectToSpace(savedObject: any): Space {
|
||||
return {
|
||||
id: savedObject.id,
|
||||
...savedObject.attributes,
|
||||
} as Space;
|
||||
}
|
||||
}
|
|
@ -4,31 +4,26 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { DEFAULT_SPACE_ID } from '../../common/constants';
|
||||
import { createSpacesTutorialContextFactory } from './spaces_tutorial_context_factory';
|
||||
import { SpacesService } from '../spaces_service';
|
||||
import { SpacesAuditLogger } from './audit_logger';
|
||||
import { coreMock, loggingSystemMock } from '../../../../../src/core/server/mocks';
|
||||
import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks';
|
||||
import { spacesServiceMock } from '../spaces_service/spaces_service.mock';
|
||||
import { spacesConfig } from './__fixtures__';
|
||||
import { securityMock } from '../../../security/server/mocks';
|
||||
import { spacesClientServiceMock } from '../spaces_client/spaces_client_service.mock';
|
||||
|
||||
const log = loggingSystemMock.createLogger();
|
||||
|
||||
const service = new SpacesService(log);
|
||||
const service = new SpacesService();
|
||||
|
||||
describe('createSpacesTutorialContextFactory', () => {
|
||||
it('should create a valid context factory', async () => {
|
||||
const spacesService = spacesServiceMock.createSetupContract();
|
||||
expect(typeof createSpacesTutorialContextFactory(spacesService)).toEqual('function');
|
||||
const spacesService = spacesServiceMock.createStartContract();
|
||||
expect(typeof createSpacesTutorialContextFactory(() => spacesService)).toEqual('function');
|
||||
});
|
||||
|
||||
it('should create context with the current space id for space my-space-id', async () => {
|
||||
const spacesService = spacesServiceMock.createSetupContract('my-space-id');
|
||||
const contextFactory = createSpacesTutorialContextFactory(spacesService);
|
||||
const spacesService = spacesServiceMock.createStartContract('my-space-id');
|
||||
const contextFactory = createSpacesTutorialContextFactory(() => spacesService);
|
||||
|
||||
const request = {};
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
expect(contextFactory(request)).toEqual({
|
||||
spaceId: 'my-space-id',
|
||||
|
@ -37,16 +32,17 @@ describe('createSpacesTutorialContextFactory', () => {
|
|||
});
|
||||
|
||||
it('should create context with the current space id for the default space', async () => {
|
||||
const spacesService = await service.setup({
|
||||
http: coreMock.createSetup().http,
|
||||
getStartServices: async () => [coreMock.createStart(), {}, {}],
|
||||
authorization: securityMock.createSetup().authz,
|
||||
auditLogger: {} as SpacesAuditLogger,
|
||||
config$: Rx.of(spacesConfig),
|
||||
service.setup({
|
||||
basePath: coreMock.createSetup().http.basePath,
|
||||
});
|
||||
const contextFactory = createSpacesTutorialContextFactory(spacesService);
|
||||
const contextFactory = createSpacesTutorialContextFactory(() =>
|
||||
service.start({
|
||||
basePath: coreMock.createStart().http.basePath,
|
||||
spacesClientService: spacesClientServiceMock.createStart(),
|
||||
})
|
||||
);
|
||||
|
||||
const request = {};
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
expect(contextFactory(request)).toEqual({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SpacesServiceSetup } from '../spaces_service/spaces_service';
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { SpacesServiceStart } from '../spaces_service/spaces_service';
|
||||
|
||||
export function createSpacesTutorialContextFactory(spacesService: SpacesServiceSetup) {
|
||||
return function spacesTutorialContextFactory(request: any) {
|
||||
export function createSpacesTutorialContextFactory(getSpacesService: () => SpacesServiceStart) {
|
||||
return function spacesTutorialContextFactory(request: KibanaRequest) {
|
||||
const spacesService = getSpacesService();
|
||||
return {
|
||||
spaceId: spacesService.getSpaceId(request),
|
||||
isInDefaultSpace: spacesService.isInDefaultSpace(request),
|
||||
|
|
|
@ -3,12 +3,25 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { spacesClientServiceMock } from './spaces_client/spaces_client_service.mock';
|
||||
import { spacesServiceMock } from './spaces_service/spaces_service.mock';
|
||||
|
||||
function createSetupMock() {
|
||||
return { spacesService: spacesServiceMock.createSetupContract() };
|
||||
return {
|
||||
spacesService: spacesServiceMock.createSetupContract(),
|
||||
spacesClient: spacesClientServiceMock.createSetup(),
|
||||
};
|
||||
}
|
||||
|
||||
function createStartMock() {
|
||||
return {
|
||||
spacesService: spacesServiceMock.createStartContract(),
|
||||
};
|
||||
}
|
||||
|
||||
export const spacesMock = {
|
||||
createSetup: createSetupMock,
|
||||
createStart: createStartMock,
|
||||
};
|
||||
|
||||
export { spacesClientMock } from './spaces_client/spaces_client.mock';
|
||||
|
|
|
@ -13,30 +13,30 @@ import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collect
|
|||
|
||||
describe('Spaces Plugin', () => {
|
||||
describe('#setup', () => {
|
||||
it('can setup with all optional plugins disabled, exposing the expected contract', async () => {
|
||||
it('can setup with all optional plugins disabled, exposing the expected contract', () => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext({});
|
||||
const core = coreMock.createSetup() as CoreSetup<PluginsStart>;
|
||||
const features = featuresPluginMock.createSetup();
|
||||
const licensing = licensingMock.createSetup();
|
||||
|
||||
const plugin = new Plugin(initializerContext);
|
||||
const spacesSetup = await plugin.setup(core, { features, licensing });
|
||||
const spacesSetup = plugin.setup(core, { features, licensing });
|
||||
expect(spacesSetup).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"spacesClient": Object {
|
||||
"registerClientWrapper": [Function],
|
||||
"setClientRepositoryFactory": [Function],
|
||||
},
|
||||
"spacesService": Object {
|
||||
"getActiveSpace": [Function],
|
||||
"getBasePath": [Function],
|
||||
"getSpaceId": [Function],
|
||||
"isInDefaultSpace": [Function],
|
||||
"namespaceToSpaceId": [Function],
|
||||
"scopedClient": [Function],
|
||||
"spaceIdToNamespace": [Function],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('registers the capabilities provider and switcher', async () => {
|
||||
it('registers the capabilities provider and switcher', () => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext({});
|
||||
const core = coreMock.createSetup() as CoreSetup<PluginsStart>;
|
||||
const features = featuresPluginMock.createSetup();
|
||||
|
@ -44,13 +44,13 @@ describe('Spaces Plugin', () => {
|
|||
|
||||
const plugin = new Plugin(initializerContext);
|
||||
|
||||
await plugin.setup(core, { features, licensing });
|
||||
plugin.setup(core, { features, licensing });
|
||||
|
||||
expect(core.capabilities.registerProvider).toHaveBeenCalledTimes(1);
|
||||
expect(core.capabilities.registerSwitcher).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('registers the usage collector', async () => {
|
||||
it('registers the usage collector', () => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext({});
|
||||
const core = coreMock.createSetup() as CoreSetup<PluginsStart>;
|
||||
const features = featuresPluginMock.createSetup();
|
||||
|
@ -60,12 +60,12 @@ describe('Spaces Plugin', () => {
|
|||
|
||||
const plugin = new Plugin(initializerContext);
|
||||
|
||||
await plugin.setup(core, { features, licensing, usageCollection });
|
||||
plugin.setup(core, { features, licensing, usageCollection });
|
||||
|
||||
expect(usageCollection.getCollectorByType('spaces')).toBeDefined();
|
||||
});
|
||||
|
||||
it('registers the "space" saved object type and client wrapper', async () => {
|
||||
it('registers the "space" saved object type and client wrapper', () => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext({});
|
||||
const core = coreMock.createSetup() as CoreSetup<PluginsStart>;
|
||||
const features = featuresPluginMock.createSetup();
|
||||
|
@ -73,7 +73,7 @@ describe('Spaces Plugin', () => {
|
|||
|
||||
const plugin = new Plugin(initializerContext);
|
||||
|
||||
await plugin.setup(core, { features, licensing });
|
||||
plugin.setup(core, { features, licensing });
|
||||
|
||||
expect(core.savedObjects.registerType).toHaveBeenCalledWith({
|
||||
name: 'space',
|
||||
|
@ -90,4 +90,32 @@ describe('Spaces Plugin', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#start', () => {
|
||||
it('can start with all optional plugins disabled, exposing the expected contract', () => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext({});
|
||||
const coreSetup = coreMock.createSetup() as CoreSetup<PluginsStart>;
|
||||
const features = featuresPluginMock.createSetup();
|
||||
const licensing = licensingMock.createSetup();
|
||||
|
||||
const plugin = new Plugin(initializerContext);
|
||||
plugin.setup(coreSetup, { features, licensing });
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
const spacesStart = plugin.start(coreStart);
|
||||
expect(spacesStart).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"spacesService": Object {
|
||||
"createSpacesClient": [Function],
|
||||
"getActiveSpace": [Function],
|
||||
"getSpaceId": [Function],
|
||||
"isInDefaultSpace": [Function],
|
||||
"namespaceToSpaceId": [Function],
|
||||
"spaceIdToNamespace": [Function],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,17 +7,20 @@
|
|||
import { Observable } from 'rxjs';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { HomeServerPluginSetup } from 'src/plugins/home/server';
|
||||
import { CoreSetup, Logger, PluginInitializerContext } from '../../../../src/core/server';
|
||||
import {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
Logger,
|
||||
PluginInitializerContext,
|
||||
} from '../../../../src/core/server';
|
||||
import {
|
||||
PluginSetupContract as FeaturesPluginSetup,
|
||||
PluginStartContract as FeaturesPluginStart,
|
||||
} from '../../features/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { SpacesAuditLogger } from './lib/audit_logger';
|
||||
import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory';
|
||||
import { registerSpacesUsageCollector } from './usage_collection';
|
||||
import { SpacesService } from './spaces_service';
|
||||
import { SpacesService, SpacesServiceStart } from './spaces_service';
|
||||
import { SpacesServiceSetup } from './spaces_service';
|
||||
import { ConfigType } from './config';
|
||||
import { initSpacesRequestInterceptors } from './lib/request_interceptors';
|
||||
|
@ -28,11 +31,15 @@ import { setupCapabilities } from './capabilities';
|
|||
import { SpacesSavedObjectsService } from './saved_objects';
|
||||
import { DefaultSpaceService } from './default_space';
|
||||
import { SpacesLicenseService } from '../common/licensing';
|
||||
import {
|
||||
SpacesClientRepositoryFactory,
|
||||
SpacesClientService,
|
||||
SpacesClientWrapper,
|
||||
} from './spaces_client';
|
||||
|
||||
export interface PluginsSetup {
|
||||
features: FeaturesPluginSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
home?: HomeServerPluginSetup;
|
||||
}
|
||||
|
@ -43,11 +50,17 @@ export interface PluginsStart {
|
|||
|
||||
export interface SpacesPluginSetup {
|
||||
spacesService: SpacesServiceSetup;
|
||||
spacesClient: {
|
||||
setClientRepositoryFactory: (factory: SpacesClientRepositoryFactory) => void;
|
||||
registerClientWrapper: (wrapper: SpacesClientWrapper) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SpacesPluginStart {
|
||||
spacesService: SpacesServiceStart;
|
||||
}
|
||||
|
||||
export class Plugin {
|
||||
private readonly pluginId = 'spaces';
|
||||
|
||||
private readonly config$: Observable<ConfigType>;
|
||||
|
||||
private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>;
|
||||
|
@ -56,32 +69,38 @@ export class Plugin {
|
|||
|
||||
private readonly spacesLicenseService = new SpacesLicenseService();
|
||||
|
||||
private readonly spacesClientService: SpacesClientService;
|
||||
|
||||
private readonly spacesService: SpacesService;
|
||||
|
||||
private spacesServiceStart?: SpacesServiceStart;
|
||||
|
||||
private defaultSpaceService?: DefaultSpaceService;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.config$ = initializerContext.config.create<ConfigType>();
|
||||
this.kibanaIndexConfig$ = initializerContext.config.legacy.globalConfig$;
|
||||
this.log = initializerContext.logger.get();
|
||||
this.spacesService = new SpacesService();
|
||||
this.spacesClientService = new SpacesClientService((message) => this.log.debug(message));
|
||||
}
|
||||
|
||||
public async start() {}
|
||||
public setup(core: CoreSetup<PluginsStart>, plugins: PluginsSetup): SpacesPluginSetup {
|
||||
const spacesClientSetup = this.spacesClientService.setup({ config$: this.config$ });
|
||||
|
||||
public async setup(
|
||||
core: CoreSetup<PluginsStart>,
|
||||
plugins: PluginsSetup
|
||||
): Promise<SpacesPluginSetup> {
|
||||
const service = new SpacesService(this.log);
|
||||
|
||||
const spacesService = await service.setup({
|
||||
http: core.http,
|
||||
getStartServices: core.getStartServices,
|
||||
authorization: plugins.security ? plugins.security.authz : null,
|
||||
auditLogger: new SpacesAuditLogger(plugins.security?.audit.getLogger(this.pluginId)),
|
||||
config$: this.config$,
|
||||
const spacesServiceSetup = this.spacesService.setup({
|
||||
basePath: core.http.basePath,
|
||||
});
|
||||
|
||||
const getSpacesService = () => {
|
||||
if (!this.spacesServiceStart) {
|
||||
throw new Error('spaces service has not been initialized!');
|
||||
}
|
||||
return this.spacesServiceStart;
|
||||
};
|
||||
|
||||
const savedObjectsService = new SpacesSavedObjectsService();
|
||||
savedObjectsService.setup({ core, spacesService });
|
||||
savedObjectsService.setup({ core, getSpacesService });
|
||||
|
||||
const { license } = this.spacesLicenseService.setup({ license$: plugins.licensing.license$ });
|
||||
|
||||
|
@ -106,24 +125,23 @@ export class Plugin {
|
|||
log: this.log,
|
||||
getStartServices: core.getStartServices,
|
||||
getImportExportObjectLimit: core.savedObjects.getImportExportObjectLimit,
|
||||
spacesService,
|
||||
authorization: plugins.security ? plugins.security.authz : null,
|
||||
getSpacesService,
|
||||
});
|
||||
|
||||
const internalRouter = core.http.createRouter();
|
||||
initInternalSpacesApi({
|
||||
internalRouter,
|
||||
spacesService,
|
||||
getSpacesService,
|
||||
});
|
||||
|
||||
initSpacesRequestInterceptors({
|
||||
http: core.http,
|
||||
log: this.log,
|
||||
spacesService,
|
||||
getSpacesService,
|
||||
features: plugins.features,
|
||||
});
|
||||
|
||||
setupCapabilities(core, spacesService, this.log);
|
||||
setupCapabilities(core, getSpacesService, this.log);
|
||||
|
||||
if (plugins.usageCollection) {
|
||||
registerSpacesUsageCollector(plugins.usageCollection, {
|
||||
|
@ -133,18 +151,28 @@ export class Plugin {
|
|||
});
|
||||
}
|
||||
|
||||
if (plugins.security) {
|
||||
plugins.security.registerSpacesService(spacesService);
|
||||
}
|
||||
|
||||
if (plugins.home) {
|
||||
plugins.home.tutorials.addScopedTutorialContextFactory(
|
||||
createSpacesTutorialContextFactory(spacesService)
|
||||
createSpacesTutorialContextFactory(getSpacesService)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
spacesService,
|
||||
spacesClient: spacesClientSetup,
|
||||
spacesService: spacesServiceSetup,
|
||||
};
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
const spacesClientStart = this.spacesClientService.start(core);
|
||||
|
||||
this.spacesServiceStart = this.spacesService.start({
|
||||
basePath: core.http.basePath,
|
||||
spacesClientService: spacesClientStart,
|
||||
});
|
||||
|
||||
return {
|
||||
spacesService: this.spacesServiceStart,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from 'src/core/server';
|
||||
import { ISavedObjectsRepository, SavedObjectsErrorHelpers } from 'src/core/server';
|
||||
|
||||
export const createMockSavedObjectsRepository = (spaces: any[] = []) => {
|
||||
const mockSavedObjectsClientContract = ({
|
||||
|
@ -37,7 +37,7 @@ export const createMockSavedObjectsRepository = (spaces: any[] = []) => {
|
|||
return {};
|
||||
}),
|
||||
deleteByNamespace: jest.fn(),
|
||||
} as unknown) as jest.Mocked<SavedObjectsClientContract>;
|
||||
} as unknown) as jest.Mocked<ISavedObjectsRepository>;
|
||||
|
||||
return mockSavedObjectsClientContract;
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
createResolveSavedObjectsImportErrorsMock,
|
||||
createMockSavedObjectsService,
|
||||
} from '../__fixtures__';
|
||||
import { CoreSetup, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
|
||||
import { kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
|
||||
import {
|
||||
loggingSystemMock,
|
||||
httpServiceMock,
|
||||
|
@ -22,11 +22,8 @@ import {
|
|||
coreMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
import { SpacesAuditLogger } from '../../../lib/audit_logger';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { initCopyToSpacesApi } from './copy_to_space';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { securityMock } from '../../../../../security/server/mocks';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
jest.mock('../../../../../../../src/core/server', () => {
|
||||
return {
|
||||
|
@ -41,6 +38,7 @@ import {
|
|||
importSavedObjectsFromStream,
|
||||
resolveSavedObjectsImportErrors,
|
||||
} from '../../../../../../../src/core/server';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
|
||||
describe('copy to space', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -74,27 +72,21 @@ describe('copy to space', () => {
|
|||
const { savedObjects } = createMockSavedObjectsService(spaces);
|
||||
coreStart.savedObjects = savedObjects;
|
||||
|
||||
const service = new SpacesService(log);
|
||||
const spacesService = await service.setup({
|
||||
http: (httpService as unknown) as CoreSetup['http'],
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
authorization: securityMock.createSetup().authz,
|
||||
auditLogger: {} as SpacesAuditLogger,
|
||||
config$: Rx.of(spacesConfig),
|
||||
const clientService = new SpacesClientService(jest.fn());
|
||||
clientService
|
||||
.setup({ config$: Rx.of(spacesConfig) })
|
||||
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
|
||||
|
||||
const service = new SpacesService();
|
||||
service.setup({
|
||||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn((req: any) => {
|
||||
return Promise.resolve(
|
||||
new SpacesClient(
|
||||
null as any,
|
||||
() => null,
|
||||
null,
|
||||
savedObjectsRepositoryMock,
|
||||
spacesConfig,
|
||||
savedObjectsRepositoryMock,
|
||||
req
|
||||
)
|
||||
);
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
basePath: coreStart.http.basePath,
|
||||
spacesClientService: clientServiceStart,
|
||||
});
|
||||
|
||||
initCopyToSpacesApi({
|
||||
|
@ -102,8 +94,7 @@ describe('copy to space', () => {
|
|||
getStartServices: async () => [coreStart, {}, {}],
|
||||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
spacesService,
|
||||
authorization: null, // not needed for this route
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
});
|
||||
|
||||
const [
|
||||
|
|
|
@ -21,7 +21,7 @@ const areObjectsUnique = (objects: SavedObjectIdentifier[]) =>
|
|||
_.uniqBy(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length;
|
||||
|
||||
export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { externalRouter, spacesService, getImportExportObjectLimit, getStartServices } = deps;
|
||||
const { externalRouter, getSpacesService, getImportExportObjectLimit, getStartServices } = deps;
|
||||
|
||||
externalRouter.post(
|
||||
{
|
||||
|
@ -90,7 +90,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
|||
overwrite,
|
||||
createNewCopies,
|
||||
} = request.body;
|
||||
const sourceSpaceId = spacesService.getSpaceId(request);
|
||||
const sourceSpaceId = getSpacesService().getSpaceId(request);
|
||||
const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, {
|
||||
objects,
|
||||
includeReferences,
|
||||
|
@ -155,7 +155,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
|||
request
|
||||
);
|
||||
const { objects, includeReferences, retries, createNewCopies } = request.body;
|
||||
const sourceSpaceId = spacesService.getSpaceId(request);
|
||||
const sourceSpaceId = getSpacesService().getSpaceId(request);
|
||||
const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts(
|
||||
sourceSpaceId,
|
||||
{
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
mockRouteContextWithInvalidLicense,
|
||||
} from '../__fixtures__';
|
||||
import {
|
||||
CoreSetup,
|
||||
kibanaResponseFactory,
|
||||
RouteValidatorConfig,
|
||||
SavedObjectsErrorHelpers,
|
||||
|
@ -24,12 +23,10 @@ import {
|
|||
coreMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
import { SpacesAuditLogger } from '../../../lib/audit_logger';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { initDeleteSpacesApi } from './delete';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { securityMock } from '../../../../../security/server/mocks';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
|
||||
describe('Spaces Public API', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -44,27 +41,21 @@ describe('Spaces Public API', () => {
|
|||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
const service = new SpacesService(log);
|
||||
const spacesService = await service.setup({
|
||||
http: (httpService as unknown) as CoreSetup['http'],
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
authorization: securityMock.createSetup().authz,
|
||||
auditLogger: {} as SpacesAuditLogger,
|
||||
config$: Rx.of(spacesConfig),
|
||||
const clientService = new SpacesClientService(jest.fn());
|
||||
clientService
|
||||
.setup({ config$: Rx.of(spacesConfig) })
|
||||
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
|
||||
|
||||
const service = new SpacesService();
|
||||
service.setup({
|
||||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn((req: any) => {
|
||||
return Promise.resolve(
|
||||
new SpacesClient(
|
||||
null as any,
|
||||
() => null,
|
||||
null,
|
||||
savedObjectsRepositoryMock,
|
||||
spacesConfig,
|
||||
savedObjectsRepositoryMock,
|
||||
req
|
||||
)
|
||||
);
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
basePath: coreStart.http.basePath,
|
||||
spacesClientService: clientServiceStart,
|
||||
});
|
||||
|
||||
initDeleteSpacesApi({
|
||||
|
@ -72,8 +63,7 @@ describe('Spaces Public API', () => {
|
|||
getStartServices: async () => [coreStart, {}, {}],
|
||||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
spacesService,
|
||||
authorization: null, // not needed for this route
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
});
|
||||
|
||||
const [routeDefinition, routeHandler] = router.delete.mock.calls[0];
|
||||
|
@ -186,6 +176,6 @@ describe('Spaces Public API', () => {
|
|||
const { status, payload } = response;
|
||||
|
||||
expect(status).toEqual(400);
|
||||
expect(payload.message).toEqual('This Space cannot be deleted because it is reserved.');
|
||||
expect(payload.message).toEqual('The default space cannot be deleted because it is reserved.');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,12 +8,11 @@ import Boom from '@hapi/boom';
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server';
|
||||
import { wrapError } from '../../../lib/errors';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { ExternalRouteDeps } from '.';
|
||||
import { createLicensedRouteHandler } from '../../lib';
|
||||
|
||||
export function initDeleteSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { externalRouter, log, spacesService } = deps;
|
||||
const { externalRouter, log, getSpacesService } = deps;
|
||||
|
||||
externalRouter.delete(
|
||||
{
|
||||
|
@ -25,7 +24,7 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) {
|
|||
},
|
||||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
const spacesClient: SpacesClient = await spacesService.scopedClient(request);
|
||||
const spacesClient = getSpacesService().createSpacesClient(request);
|
||||
|
||||
const id = request.params.id;
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
mockRouteContext,
|
||||
} from '../__fixtures__';
|
||||
import { initGetSpaceApi } from './get';
|
||||
import { CoreSetup, kibanaResponseFactory } from 'src/core/server';
|
||||
import { kibanaResponseFactory } from 'src/core/server';
|
||||
import {
|
||||
loggingSystemMock,
|
||||
httpServiceMock,
|
||||
|
@ -19,10 +19,8 @@ import {
|
|||
coreMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
import { SpacesAuditLogger } from '../../../lib/audit_logger';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { securityMock } from '../../../../../security/server/mocks';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
|
||||
describe('GET space', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -38,27 +36,21 @@ describe('GET space', () => {
|
|||
|
||||
const log = loggingSystemMock.create().get('spaces');
|
||||
|
||||
const service = new SpacesService(log);
|
||||
const spacesService = await service.setup({
|
||||
http: (httpService as unknown) as CoreSetup['http'],
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
authorization: securityMock.createSetup().authz,
|
||||
auditLogger: {} as SpacesAuditLogger,
|
||||
config$: Rx.of(spacesConfig),
|
||||
const clientService = new SpacesClientService(jest.fn());
|
||||
clientService
|
||||
.setup({ config$: Rx.of(spacesConfig) })
|
||||
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
|
||||
|
||||
const service = new SpacesService();
|
||||
service.setup({
|
||||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn((req: any) => {
|
||||
return Promise.resolve(
|
||||
new SpacesClient(
|
||||
null as any,
|
||||
() => null,
|
||||
null,
|
||||
savedObjectsRepositoryMock,
|
||||
spacesConfig,
|
||||
savedObjectsRepositoryMock,
|
||||
req
|
||||
)
|
||||
);
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
basePath: coreStart.http.basePath,
|
||||
spacesClientService: clientServiceStart,
|
||||
});
|
||||
|
||||
initGetSpaceApi({
|
||||
|
@ -66,8 +58,7 @@ describe('GET space', () => {
|
|||
getStartServices: async () => [coreStart, {}, {}],
|
||||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
spacesService,
|
||||
authorization: null, // not needed for this route
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ExternalRouteDeps } from '.';
|
|||
import { createLicensedRouteHandler } from '../../lib';
|
||||
|
||||
export function initGetSpaceApi(deps: ExternalRouteDeps) {
|
||||
const { externalRouter, spacesService } = deps;
|
||||
const { externalRouter, getSpacesService } = deps;
|
||||
|
||||
externalRouter.get(
|
||||
{
|
||||
|
@ -24,7 +24,7 @@ export function initGetSpaceApi(deps: ExternalRouteDeps) {
|
|||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
const spaceId = request.params.id;
|
||||
const spacesClient = await spacesService.scopedClient(request);
|
||||
const spacesClient = getSpacesService().createSpacesClient(request);
|
||||
|
||||
try {
|
||||
const space = await spacesClient.get(spaceId);
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
mockRouteContext,
|
||||
mockRouteContextWithInvalidLicense,
|
||||
} from '../__fixtures__';
|
||||
import { CoreSetup, kibanaResponseFactory } from 'src/core/server';
|
||||
import { kibanaResponseFactory } from 'src/core/server';
|
||||
import {
|
||||
loggingSystemMock,
|
||||
httpServiceMock,
|
||||
|
@ -18,11 +18,10 @@ import {
|
|||
coreMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
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';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
|
||||
describe('GET /spaces/space', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -38,27 +37,21 @@ describe('GET /spaces/space', () => {
|
|||
|
||||
const log = loggingSystemMock.create().get('spaces');
|
||||
|
||||
const service = new SpacesService(log);
|
||||
const spacesService = await service.setup({
|
||||
http: (httpService as unknown) as CoreSetup['http'],
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
authorization: securityMock.createSetup().authz,
|
||||
auditLogger: {} as SpacesAuditLogger,
|
||||
config$: Rx.of(spacesConfig),
|
||||
const clientService = new SpacesClientService(jest.fn());
|
||||
clientService
|
||||
.setup({ config$: Rx.of(spacesConfig) })
|
||||
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
|
||||
|
||||
const service = new SpacesService();
|
||||
service.setup({
|
||||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn((req: any) => {
|
||||
return Promise.resolve(
|
||||
new SpacesClient(
|
||||
null as any,
|
||||
() => null,
|
||||
null,
|
||||
savedObjectsRepositoryMock,
|
||||
spacesConfig,
|
||||
savedObjectsRepositoryMock,
|
||||
req
|
||||
)
|
||||
);
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
basePath: coreStart.http.basePath,
|
||||
spacesClientService: clientServiceStart,
|
||||
});
|
||||
|
||||
initGetAllSpacesApi({
|
||||
|
@ -66,11 +59,11 @@ describe('GET /spaces/space', () => {
|
|||
getStartServices: async () => [coreStart, {}, {}],
|
||||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
spacesService,
|
||||
authorization: null, // not needed for this route
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
});
|
||||
|
||||
return {
|
||||
routeConfig: router.get.mock.calls[0][0],
|
||||
routeHandler: router.get.mock.calls[0][1],
|
||||
};
|
||||
};
|
||||
|
@ -89,21 +82,27 @@ describe('GET /spaces/space', () => {
|
|||
});
|
||||
|
||||
it(`returns expected result when specifying include_authorized_purposes=true`, async () => {
|
||||
const { routeHandler } = await setup();
|
||||
const { routeConfig, routeHandler } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
method: 'get',
|
||||
query: { purpose, include_authorized_purposes: true },
|
||||
});
|
||||
|
||||
if (routeConfig.validate === false) {
|
||||
throw new Error('Test setup failure. Expected route validation');
|
||||
}
|
||||
const queryParamsValidation = routeConfig.validate.query! as ObjectType<any>;
|
||||
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
if (purpose === undefined) {
|
||||
expect(() => queryParamsValidation.validate(request.query)).not.toThrow();
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload).toEqual(spaces);
|
||||
} else {
|
||||
expect(response.status).toEqual(400);
|
||||
expect(response.payload).toEqual(
|
||||
new Error(`'purpose' cannot be supplied with 'includeAuthorizedPurposes'`)
|
||||
expect(() => queryParamsValidation.validate(request.query)).toThrowError(
|
||||
'[include_authorized_purposes]: expected value to equal [false]'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ExternalRouteDeps } from '.';
|
|||
import { createLicensedRouteHandler } from '../../lib';
|
||||
|
||||
export function initGetAllSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { externalRouter, log, spacesService } = deps;
|
||||
const { externalRouter, log, getSpacesService } = deps;
|
||||
|
||||
externalRouter.get(
|
||||
{
|
||||
|
@ -39,7 +39,7 @@ export function initGetAllSpacesApi(deps: ExternalRouteDeps) {
|
|||
|
||||
const { purpose, include_authorized_purposes: includeAuthorizedPurposes } = request.query;
|
||||
|
||||
const spacesClient = await spacesService.scopedClient(request);
|
||||
const spacesClient = getSpacesService().createSpacesClient(request);
|
||||
|
||||
let spaces: Space[];
|
||||
|
||||
|
|
|
@ -5,13 +5,12 @@
|
|||
*/
|
||||
|
||||
import { Logger, IRouter, CoreSetup } from 'src/core/server';
|
||||
import { SecurityPluginSetup } from '../../../../../security/server';
|
||||
import { initDeleteSpacesApi } from './delete';
|
||||
import { initGetSpaceApi } from './get';
|
||||
import { initGetAllSpacesApi } from './get_all';
|
||||
import { initPostSpacesApi } from './post';
|
||||
import { initPutSpacesApi } from './put';
|
||||
import { SpacesServiceSetup } from '../../../spaces_service/spaces_service';
|
||||
import { SpacesServiceStart } from '../../../spaces_service/spaces_service';
|
||||
import { initCopyToSpacesApi } from './copy_to_space';
|
||||
import { initShareToSpacesApi } from './share_to_space';
|
||||
|
||||
|
@ -19,9 +18,8 @@ export interface ExternalRouteDeps {
|
|||
externalRouter: IRouter;
|
||||
getStartServices: CoreSetup['getStartServices'];
|
||||
getImportExportObjectLimit: () => number;
|
||||
spacesService: SpacesServiceSetup;
|
||||
getSpacesService: () => SpacesServiceStart;
|
||||
log: Logger;
|
||||
authorization: SecurityPluginSetup['authz'] | null;
|
||||
}
|
||||
|
||||
export function initExternalSpacesApi(deps: ExternalRouteDeps) {
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
mockRouteContext,
|
||||
mockRouteContextWithInvalidLicense,
|
||||
} from '../__fixtures__';
|
||||
import { CoreSetup, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
|
||||
import { kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
|
||||
import {
|
||||
loggingSystemMock,
|
||||
httpServerMock,
|
||||
|
@ -18,12 +18,10 @@ import {
|
|||
coreMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
import { SpacesAuditLogger } from '../../../lib/audit_logger';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { initPostSpacesApi } from './post';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { securityMock } from '../../../../../security/server/mocks';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
|
||||
describe('Spaces Public API', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -38,27 +36,21 @@ describe('Spaces Public API', () => {
|
|||
|
||||
const log = loggingSystemMock.create().get('spaces');
|
||||
|
||||
const service = new SpacesService(log);
|
||||
const spacesService = await service.setup({
|
||||
http: (httpService as unknown) as CoreSetup['http'],
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
authorization: securityMock.createSetup().authz,
|
||||
auditLogger: {} as SpacesAuditLogger,
|
||||
config$: Rx.of(spacesConfig),
|
||||
const clientService = new SpacesClientService(jest.fn());
|
||||
clientService
|
||||
.setup({ config$: Rx.of(spacesConfig) })
|
||||
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
|
||||
|
||||
const service = new SpacesService();
|
||||
service.setup({
|
||||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn((req: any) => {
|
||||
return Promise.resolve(
|
||||
new SpacesClient(
|
||||
null as any,
|
||||
() => null,
|
||||
null,
|
||||
savedObjectsRepositoryMock,
|
||||
spacesConfig,
|
||||
savedObjectsRepositoryMock,
|
||||
req
|
||||
)
|
||||
);
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
basePath: coreStart.http.basePath,
|
||||
spacesClientService: clientServiceStart,
|
||||
});
|
||||
|
||||
initPostSpacesApi({
|
||||
|
@ -66,8 +58,7 @@ describe('Spaces Public API', () => {
|
|||
getStartServices: async () => [coreStart, {}, {}],
|
||||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
spacesService,
|
||||
authorization: null, // not needed for this route
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
});
|
||||
|
||||
const [routeDefinition, routeHandler] = router.post.mock.calls[0];
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ExternalRouteDeps } from '.';
|
|||
import { createLicensedRouteHandler } from '../../lib';
|
||||
|
||||
export function initPostSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { externalRouter, log, spacesService } = deps;
|
||||
const { externalRouter, log, getSpacesService } = deps;
|
||||
|
||||
externalRouter.post(
|
||||
{
|
||||
|
@ -22,7 +22,7 @@ export function initPostSpacesApi(deps: ExternalRouteDeps) {
|
|||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
log.debug(`Inside POST /api/spaces/space`);
|
||||
const spacesClient = await spacesService.scopedClient(request);
|
||||
const spacesClient = getSpacesService().createSpacesClient(request);
|
||||
|
||||
const space = request.body;
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
mockRouteContext,
|
||||
mockRouteContextWithInvalidLicense,
|
||||
} from '../__fixtures__';
|
||||
import { CoreSetup, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
|
||||
import { kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
|
||||
import {
|
||||
loggingSystemMock,
|
||||
httpServiceMock,
|
||||
|
@ -19,12 +19,10 @@ import {
|
|||
coreMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
import { SpacesAuditLogger } from '../../../lib/audit_logger';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { initPutSpacesApi } from './put';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { securityMock } from '../../../../../security/server/mocks';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
|
||||
describe('PUT /api/spaces/space', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -39,27 +37,21 @@ describe('PUT /api/spaces/space', () => {
|
|||
|
||||
const log = loggingSystemMock.create().get('spaces');
|
||||
|
||||
const service = new SpacesService(log);
|
||||
const spacesService = await service.setup({
|
||||
http: (httpService as unknown) as CoreSetup['http'],
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
authorization: securityMock.createSetup().authz,
|
||||
auditLogger: {} as SpacesAuditLogger,
|
||||
config$: Rx.of(spacesConfig),
|
||||
const clientService = new SpacesClientService(jest.fn());
|
||||
clientService
|
||||
.setup({ config$: Rx.of(spacesConfig) })
|
||||
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
|
||||
|
||||
const service = new SpacesService();
|
||||
service.setup({
|
||||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn((req: any) => {
|
||||
return Promise.resolve(
|
||||
new SpacesClient(
|
||||
null as any,
|
||||
() => null,
|
||||
null,
|
||||
savedObjectsRepositoryMock,
|
||||
spacesConfig,
|
||||
savedObjectsRepositoryMock,
|
||||
req
|
||||
)
|
||||
);
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
basePath: coreStart.http.basePath,
|
||||
spacesClientService: clientServiceStart,
|
||||
});
|
||||
|
||||
initPutSpacesApi({
|
||||
|
@ -67,8 +59,7 @@ describe('PUT /api/spaces/space', () => {
|
|||
getStartServices: async () => [coreStart, {}, {}],
|
||||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
spacesService,
|
||||
authorization: null, // not needed for this route
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
});
|
||||
|
||||
const [routeDefinition, routeHandler] = router.put.mock.calls[0];
|
||||
|
|
|
@ -13,7 +13,7 @@ import { ExternalRouteDeps } from '.';
|
|||
import { createLicensedRouteHandler } from '../../lib';
|
||||
|
||||
export function initPutSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { externalRouter, spacesService } = deps;
|
||||
const { externalRouter, getSpacesService } = deps;
|
||||
|
||||
externalRouter.put(
|
||||
{
|
||||
|
@ -26,7 +26,7 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) {
|
|||
},
|
||||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
const spacesClient = await spacesService.scopedClient(request);
|
||||
const spacesClient = getSpacesService().createSpacesClient(request);
|
||||
|
||||
const space = request.body;
|
||||
const id = request.params.id;
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
mockRouteContextWithInvalidLicense,
|
||||
createMockSavedObjectsService,
|
||||
} from '../__fixtures__';
|
||||
import { CoreSetup, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
|
||||
import { kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
|
||||
import {
|
||||
loggingSystemMock,
|
||||
httpServiceMock,
|
||||
|
@ -19,21 +19,16 @@ import {
|
|||
coreMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
import { SpacesAuditLogger } from '../../../lib/audit_logger';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { initShareToSpacesApi } from './share_to_space';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { securityMock } from '../../../../../security/server/mocks';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { SecurityPluginSetup } from '../../../../../security/server';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
|
||||
describe('share to space', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
const spaces = spacesSavedObjects.map((s) => ({ id: s.id, ...s.attributes }));
|
||||
|
||||
const setup = async ({
|
||||
authorization = null,
|
||||
}: { authorization?: SecurityPluginSetup['authz'] | null } = {}) => {
|
||||
const setup = async () => {
|
||||
const httpService = httpServiceMock.createSetupContract();
|
||||
const router = httpService.createRouter();
|
||||
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects);
|
||||
|
@ -42,36 +37,28 @@ describe('share to space', () => {
|
|||
const { savedObjects, savedObjectsClient } = createMockSavedObjectsService(spaces);
|
||||
coreStart.savedObjects = savedObjects;
|
||||
|
||||
const service = new SpacesService(log);
|
||||
const spacesService = await service.setup({
|
||||
http: (httpService as unknown) as CoreSetup['http'],
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
authorization,
|
||||
auditLogger: {} as SpacesAuditLogger,
|
||||
config$: Rx.of(spacesConfig),
|
||||
const clientService = new SpacesClientService(jest.fn());
|
||||
clientService
|
||||
.setup({ config$: Rx.of(spacesConfig) })
|
||||
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
|
||||
|
||||
const service = new SpacesService();
|
||||
service.setup({
|
||||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn((req: any) => {
|
||||
return Promise.resolve(
|
||||
new SpacesClient(
|
||||
null as any,
|
||||
() => null,
|
||||
null,
|
||||
savedObjectsRepositoryMock,
|
||||
spacesConfig,
|
||||
savedObjectsRepositoryMock,
|
||||
req
|
||||
)
|
||||
);
|
||||
});
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
basePath: coreStart.http.basePath,
|
||||
spacesClientService: clientServiceStart,
|
||||
});
|
||||
initShareToSpacesApi({
|
||||
externalRouter: router,
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
spacesService,
|
||||
authorization,
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
});
|
||||
|
||||
const [
|
||||
|
@ -79,8 +66,6 @@ describe('share to space', () => {
|
|||
[shareRemove, resolveRouteHandler],
|
||||
] = router.post.mock.calls;
|
||||
|
||||
const [[, permissionsRouteHandler]] = router.get.mock.calls;
|
||||
|
||||
return {
|
||||
coreStart,
|
||||
savedObjectsClient,
|
||||
|
@ -92,76 +77,10 @@ describe('share to space', () => {
|
|||
routeValidation: shareRemove.validate as RouteValidatorConfig<{}, {}, {}>,
|
||||
routeHandler: resolveRouteHandler,
|
||||
},
|
||||
sharePermissions: {
|
||||
routeHandler: permissionsRouteHandler,
|
||||
},
|
||||
savedObjectsRepositoryMock,
|
||||
};
|
||||
};
|
||||
|
||||
describe('GET /internal/spaces/_share_saved_object_permissions', () => {
|
||||
it('returns true when security is not enabled', async () => {
|
||||
const { sharePermissions } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({ query: { type: 'foo' }, method: 'get' });
|
||||
const response = await sharePermissions.routeHandler(
|
||||
mockRouteContext,
|
||||
request,
|
||||
kibanaResponseFactory
|
||||
);
|
||||
|
||||
const { status, payload } = response;
|
||||
expect(status).toEqual(200);
|
||||
expect(payload).toEqual({ shareToAllSpaces: true });
|
||||
});
|
||||
|
||||
it('returns false when the user is not authorized globally', async () => {
|
||||
const authorization = securityMock.createSetup().authz;
|
||||
const globalPrivilegesCheck = jest.fn().mockResolvedValue({ hasAllRequested: false });
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({
|
||||
globally: globalPrivilegesCheck,
|
||||
});
|
||||
const { sharePermissions } = await setup({ authorization });
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({ query: { type: 'foo' }, method: 'get' });
|
||||
const response = await sharePermissions.routeHandler(
|
||||
mockRouteContext,
|
||||
request,
|
||||
kibanaResponseFactory
|
||||
);
|
||||
|
||||
const { status, payload } = response;
|
||||
expect(status).toEqual(200);
|
||||
expect(payload).toEqual({ shareToAllSpaces: false });
|
||||
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledTimes(1);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
});
|
||||
|
||||
it('returns true when the user is authorized globally', async () => {
|
||||
const authorization = securityMock.createSetup().authz;
|
||||
const globalPrivilegesCheck = jest.fn().mockResolvedValue({ hasAllRequested: true });
|
||||
authorization.checkPrivilegesWithRequest.mockReturnValue({
|
||||
globally: globalPrivilegesCheck,
|
||||
});
|
||||
const { sharePermissions } = await setup({ authorization });
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({ query: { type: 'foo' }, method: 'get' });
|
||||
const response = await sharePermissions.routeHandler(
|
||||
mockRouteContext,
|
||||
request,
|
||||
kibanaResponseFactory
|
||||
);
|
||||
|
||||
const { status, payload } = response;
|
||||
expect(status).toEqual(200);
|
||||
expect(payload).toEqual({ shareToAllSpaces: true });
|
||||
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledTimes(1);
|
||||
expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/spaces/_share_saved_object_add', () => {
|
||||
const object = { id: 'foo', type: 'bar' };
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import { createLicensedRouteHandler } from '../../lib';
|
|||
|
||||
const uniq = <T>(arr: T[]): T[] => Array.from(new Set<T>(arr));
|
||||
export function initShareToSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { externalRouter, getStartServices, authorization } = deps;
|
||||
const { externalRouter, getStartServices } = deps;
|
||||
|
||||
const shareSchema = schema.object({
|
||||
spaces: schema.arrayOf(
|
||||
|
@ -37,31 +37,6 @@ export function initShareToSpacesApi(deps: ExternalRouteDeps) {
|
|||
object: schema.object({ type: schema.string(), id: schema.string() }),
|
||||
});
|
||||
|
||||
externalRouter.get(
|
||||
{
|
||||
path: '/internal/spaces/_share_saved_object_permissions',
|
||||
validate: { query: schema.object({ type: schema.string() }) },
|
||||
},
|
||||
createLicensedRouteHandler(async (_context, request, response) => {
|
||||
let shareToAllSpaces = true;
|
||||
const { type } = request.query;
|
||||
|
||||
if (authorization) {
|
||||
try {
|
||||
const checkPrivileges = authorization.checkPrivilegesWithRequest(request);
|
||||
shareToAllSpaces = (
|
||||
await checkPrivileges.globally({
|
||||
kibana: authorization.actions.savedObject.get(type, 'share_to_space'),
|
||||
})
|
||||
).hasAllRequested;
|
||||
} catch (error) {
|
||||
return response.customError(wrapError(error));
|
||||
}
|
||||
}
|
||||
return response.ok({ body: { shareToAllSpaces } });
|
||||
})
|
||||
);
|
||||
|
||||
externalRouter.post(
|
||||
{ path: '/api/spaces/_share_saved_object_add', validate: { body: shareSchema } },
|
||||
createLicensedRouteHandler(async (_context, request, response) => {
|
||||
|
|
|
@ -3,14 +3,12 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as Rx from 'rxjs';
|
||||
import { mockRouteContextWithInvalidLicense } from '../__fixtures__';
|
||||
import { CoreSetup, kibanaResponseFactory } from 'src/core/server';
|
||||
import { kibanaResponseFactory } from 'src/core/server';
|
||||
import { httpServiceMock, httpServerMock, coreMock } from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
import { SpacesAuditLogger } from '../../../lib/audit_logger';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { initGetActiveSpaceApi } from './get_active_space';
|
||||
import { spacesClientServiceMock } from '../../../spaces_client/spaces_client_service.mock';
|
||||
|
||||
describe('GET /internal/spaces/_active_space', () => {
|
||||
const setup = async () => {
|
||||
|
@ -19,18 +17,18 @@ describe('GET /internal/spaces/_active_space', () => {
|
|||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
const service = new SpacesService(null as any);
|
||||
const spacesService = await service.setup({
|
||||
http: (httpService as unknown) as CoreSetup['http'],
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
authorization: null,
|
||||
auditLogger: {} as SpacesAuditLogger,
|
||||
config$: Rx.of(spacesConfig),
|
||||
const service = new SpacesService();
|
||||
service.setup({
|
||||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
initGetActiveSpaceApi({
|
||||
internalRouter: router,
|
||||
spacesService,
|
||||
getSpacesService: () =>
|
||||
service.start({
|
||||
basePath: coreStart.http.basePath,
|
||||
spacesClientService: spacesClientServiceMock.createStart(),
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { InternalRouteDeps } from '.';
|
|||
import { createLicensedRouteHandler } from '../../lib';
|
||||
|
||||
export function initGetActiveSpaceApi(deps: InternalRouteDeps) {
|
||||
const { internalRouter, spacesService } = deps;
|
||||
const { internalRouter, getSpacesService } = deps;
|
||||
|
||||
internalRouter.get(
|
||||
{
|
||||
|
@ -18,7 +18,7 @@ export function initGetActiveSpaceApi(deps: InternalRouteDeps) {
|
|||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
try {
|
||||
const space = await spacesService.getActiveSpace(request);
|
||||
const space = await getSpacesService().getActiveSpace(request);
|
||||
return response.ok({ body: space });
|
||||
} catch (error) {
|
||||
return response.customError(wrapError(error));
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
*/
|
||||
|
||||
import { IRouter } from 'src/core/server';
|
||||
import { SpacesServiceSetup } from '../../../spaces_service/spaces_service';
|
||||
import { SpacesServiceStart } from '../../../spaces_service/spaces_service';
|
||||
import { initGetActiveSpaceApi } from './get_active_space';
|
||||
|
||||
export interface InternalRouteDeps {
|
||||
internalRouter: IRouter;
|
||||
spacesService: SpacesServiceSetup;
|
||||
getSpacesService: () => SpacesServiceStart;
|
||||
}
|
||||
|
||||
export function initInternalSpacesApi(deps: InternalRouteDeps) {
|
||||
|
|
|
@ -9,16 +9,16 @@ import {
|
|||
SavedObjectsClientWrapperOptions,
|
||||
} from 'src/core/server';
|
||||
import { SpacesSavedObjectsClient } from './spaces_saved_objects_client';
|
||||
import { SpacesServiceSetup } from '../spaces_service/spaces_service';
|
||||
import { SpacesServiceStart } from '../spaces_service/spaces_service';
|
||||
|
||||
export function spacesSavedObjectsClientWrapperFactory(
|
||||
spacesService: SpacesServiceSetup
|
||||
getSpacesService: () => SpacesServiceStart
|
||||
): SavedObjectsClientWrapperFactory {
|
||||
return (options: SavedObjectsClientWrapperOptions) =>
|
||||
new SpacesSavedObjectsClient({
|
||||
baseClient: options.client,
|
||||
request: options.request,
|
||||
spacesService,
|
||||
getSpacesService,
|
||||
typeRegistry: options.typeRegistry,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,10 +12,10 @@ describe('SpacesSavedObjectsService', () => {
|
|||
describe('#setup', () => {
|
||||
it('registers the "space" saved object type with appropriate mappings and migrations', () => {
|
||||
const core = coreMock.createSetup();
|
||||
const spacesService = spacesServiceMock.createSetupContract();
|
||||
const spacesService = spacesServiceMock.createStartContract();
|
||||
|
||||
const service = new SpacesSavedObjectsService();
|
||||
service.setup({ core, spacesService });
|
||||
service.setup({ core, getSpacesService: () => spacesService });
|
||||
|
||||
expect(core.savedObjects.registerType).toHaveBeenCalledTimes(1);
|
||||
expect(core.savedObjects.registerType.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
|
@ -66,10 +66,10 @@ describe('SpacesSavedObjectsService', () => {
|
|||
|
||||
it('registers the client wrapper', () => {
|
||||
const core = coreMock.createSetup();
|
||||
const spacesService = spacesServiceMock.createSetupContract();
|
||||
const spacesService = spacesServiceMock.createStartContract();
|
||||
|
||||
const service = new SpacesSavedObjectsService();
|
||||
service.setup({ core, spacesService });
|
||||
service.setup({ core, getSpacesService: () => spacesService });
|
||||
|
||||
expect(core.savedObjects.addClientWrapper).toHaveBeenCalledTimes(1);
|
||||
expect(core.savedObjects.addClientWrapper).toHaveBeenCalledWith(
|
||||
|
|
|
@ -8,15 +8,15 @@ import { CoreSetup } from 'src/core/server';
|
|||
import { SpacesSavedObjectMappings } from './mappings';
|
||||
import { migrateToKibana660 } from './migrations';
|
||||
import { spacesSavedObjectsClientWrapperFactory } from './saved_objects_client_wrapper_factory';
|
||||
import { SpacesServiceSetup } from '../spaces_service';
|
||||
import { SpacesServiceStart } from '../spaces_service';
|
||||
|
||||
interface SetupDeps {
|
||||
core: Pick<CoreSetup, 'savedObjects' | 'getStartServices'>;
|
||||
spacesService: SpacesServiceSetup;
|
||||
getSpacesService: () => SpacesServiceStart;
|
||||
}
|
||||
|
||||
export class SpacesSavedObjectsService {
|
||||
public setup({ core, spacesService }: SetupDeps) {
|
||||
public setup({ core, getSpacesService }: SetupDeps) {
|
||||
core.savedObjects.registerType({
|
||||
name: 'space',
|
||||
hidden: true,
|
||||
|
@ -30,7 +30,7 @@ export class SpacesSavedObjectsService {
|
|||
core.savedObjects.addClientWrapper(
|
||||
Number.MIN_SAFE_INTEGER,
|
||||
'spaces',
|
||||
spacesSavedObjectsClientWrapperFactory(spacesService)
|
||||
spacesSavedObjectsClientWrapperFactory(getSpacesService)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client';
|
|||
import { spacesServiceMock } from '../spaces_service/spaces_service.mock';
|
||||
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
|
||||
import { SavedObjectTypeRegistry } from 'src/core/server';
|
||||
import { SpacesClient } from '../lib/spaces_client';
|
||||
import { spacesClientMock } from '../lib/spaces_client/spaces_client.mock';
|
||||
import { SpacesClient } from '../spaces_client';
|
||||
import { spacesClientMock } from '../spaces_client/spaces_client.mock';
|
||||
import Boom from '@hapi/boom';
|
||||
|
||||
const typeRegistry = new SavedObjectTypeRegistry();
|
||||
|
@ -39,8 +39,8 @@ const createMockRequest = () => ({});
|
|||
|
||||
const createMockClient = () => savedObjectsClientMock.create();
|
||||
|
||||
const createSpacesService = async (spaceId: string) => {
|
||||
return spacesServiceMock.createSetupContract(spaceId);
|
||||
const createSpacesService = (spaceId: string) => {
|
||||
return spacesServiceMock.createStartContract(spaceId);
|
||||
};
|
||||
|
||||
const createMockResponse = () => ({
|
||||
|
@ -61,15 +61,15 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
{ id: 'space_1', expectedNamespace: 'space_1' },
|
||||
].forEach((currentSpace) => {
|
||||
describe(`${currentSpace.id} space`, () => {
|
||||
const createSpacesSavedObjectsClient = async () => {
|
||||
const createSpacesSavedObjectsClient = () => {
|
||||
const request = createMockRequest();
|
||||
const baseClient = createMockClient();
|
||||
const spacesService = await createSpacesService(currentSpace.id);
|
||||
const spacesService = createSpacesService(currentSpace.id);
|
||||
|
||||
const client = new SpacesSavedObjectsClient({
|
||||
request,
|
||||
baseClient,
|
||||
spacesService,
|
||||
getSpacesService: () => spacesService,
|
||||
typeRegistry,
|
||||
});
|
||||
return { client, baseClient, spacesService };
|
||||
|
@ -77,7 +77,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#get', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(client.get('foo', '', { namespace: 'bar' })).rejects.toThrow(
|
||||
ERROR_NAMESPACE_SPECIFIED
|
||||
|
@ -85,7 +85,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = createMockResponse();
|
||||
baseClient.get.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
@ -105,7 +105,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#bulkGet', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(
|
||||
client.bulkGet([{ id: '', type: 'foo' }], { namespace: 'bar' })
|
||||
|
@ -113,7 +113,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = { saved_objects: [createMockResponse()] };
|
||||
baseClient.bulkGet.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
@ -134,10 +134,10 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
const EMPTY_RESPONSE = { saved_objects: [], total: 0, per_page: 20, page: 1 };
|
||||
|
||||
test(`returns empty result if user is unauthorized in this space`, async () => {
|
||||
const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient, spacesService } = createSpacesSavedObjectsClient();
|
||||
const spacesClient = spacesClientMock.create();
|
||||
spacesClient.getAll.mockResolvedValue([]);
|
||||
spacesService.scopedClient.mockResolvedValue(spacesClient);
|
||||
spacesService.createSpacesClient.mockReturnValue(spacesClient);
|
||||
|
||||
const options = Object.freeze({ type: 'foo', namespaces: ['some-ns'] });
|
||||
const actualReturnValue = await client.find(options);
|
||||
|
@ -147,10 +147,10 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`returns empty result if user is unauthorized in any space`, async () => {
|
||||
const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient, spacesService } = createSpacesSavedObjectsClient();
|
||||
const spacesClient = spacesClientMock.create();
|
||||
spacesClient.getAll.mockRejectedValue(Boom.unauthorized());
|
||||
spacesService.scopedClient.mockResolvedValue(spacesClient);
|
||||
spacesService.createSpacesClient.mockReturnValue(spacesClient);
|
||||
|
||||
const options = Object.freeze({ type: 'foo', namespaces: ['some-ns'] });
|
||||
const actualReturnValue = await client.find(options);
|
||||
|
@ -160,7 +160,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`passes options.type to baseClient if valid singular type specified`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })),
|
||||
total: 1,
|
||||
|
@ -180,7 +180,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })),
|
||||
total: 1,
|
||||
|
@ -200,7 +200,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`passes options.namespaces along`, async () => {
|
||||
const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient, spacesService } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()],
|
||||
total: 1,
|
||||
|
@ -209,7 +209,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
};
|
||||
baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
const spacesClient = (await spacesService.scopedClient(null as any)) as jest.Mocked<
|
||||
const spacesClient = spacesService.createSpacesClient(null as any) as jest.Mocked<
|
||||
SpacesClient
|
||||
>;
|
||||
spacesClient.getAll.mockImplementation(() =>
|
||||
|
@ -231,7 +231,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`filters options.namespaces based on authorization`, async () => {
|
||||
const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient, spacesService } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()],
|
||||
total: 1,
|
||||
|
@ -240,7 +240,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
};
|
||||
baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
const spacesClient = (await spacesService.scopedClient(null as any)) as jest.Mocked<
|
||||
const spacesClient = spacesService.createSpacesClient(null as any) as jest.Mocked<
|
||||
SpacesClient
|
||||
>;
|
||||
spacesClient.getAll.mockImplementation(() =>
|
||||
|
@ -262,7 +262,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`translates options.namespace: ['*']`, async () => {
|
||||
const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient, spacesService } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()],
|
||||
total: 1,
|
||||
|
@ -271,7 +271,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
};
|
||||
baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
const spacesClient = (await spacesService.scopedClient(null as any)) as jest.Mocked<
|
||||
const spacesClient = spacesService.createSpacesClient(null as any) as jest.Mocked<
|
||||
SpacesClient
|
||||
>;
|
||||
spacesClient.getAll.mockImplementation(() =>
|
||||
|
@ -295,7 +295,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#checkConflicts', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error
|
||||
|
@ -304,7 +304,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = { errors: [] };
|
||||
baseClient.checkConflicts.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
@ -323,7 +323,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#create', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(client.create('foo', {}, { namespace: 'bar' })).rejects.toThrow(
|
||||
ERROR_NAMESPACE_SPECIFIED
|
||||
|
@ -331,7 +331,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = createMockResponse();
|
||||
baseClient.create.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
@ -351,7 +351,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#bulkCreate', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(
|
||||
client.bulkCreate([{ id: '', type: 'foo', attributes: {} }], { namespace: 'bar' })
|
||||
|
@ -359,7 +359,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = { saved_objects: [createMockResponse()] };
|
||||
baseClient.bulkCreate.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
@ -378,7 +378,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#update', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error
|
||||
|
@ -387,7 +387,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = createMockResponse();
|
||||
baseClient.update.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
@ -408,7 +408,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#bulkUpdate', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error
|
||||
|
@ -417,7 +417,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = { saved_objects: [createMockResponse()] };
|
||||
baseClient.bulkUpdate.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
@ -442,7 +442,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#delete', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error
|
||||
|
@ -451,7 +451,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = createMockResponse();
|
||||
baseClient.delete.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
@ -471,7 +471,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#addToNamespaces', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error
|
||||
|
@ -480,7 +480,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = { namespaces: ['foo', 'bar'] };
|
||||
baseClient.addToNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
@ -501,7 +501,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#deleteFromNamespaces', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error
|
||||
|
@ -510,7 +510,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = { namespaces: ['foo', 'bar'] };
|
||||
baseClient.deleteFromNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
@ -531,7 +531,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
|
||||
describe('#removeReferencesTo', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = await createSpacesSavedObjectsClient();
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error
|
||||
|
@ -540,7 +540,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = { updated: 12 };
|
||||
baseClient.removeReferencesTo.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
|
|
|
@ -22,14 +22,14 @@ import {
|
|||
ISavedObjectTypeRegistry,
|
||||
} from '../../../../../src/core/server';
|
||||
import { ALL_SPACES_ID } from '../../common/constants';
|
||||
import { SpacesServiceSetup } from '../spaces_service/spaces_service';
|
||||
import { SpacesServiceStart } from '../spaces_service/spaces_service';
|
||||
import { spaceIdToNamespace } from '../lib/utils/namespace';
|
||||
import { SpacesClient } from '../lib/spaces_client';
|
||||
import { ISpacesClient } from '../spaces_client';
|
||||
|
||||
interface SpacesSavedObjectsClientOptions {
|
||||
baseClient: SavedObjectsClientContract;
|
||||
request: any;
|
||||
spacesService: SpacesServiceSetup;
|
||||
getSpacesService: () => SpacesServiceStart;
|
||||
typeRegistry: ISavedObjectTypeRegistry;
|
||||
}
|
||||
|
||||
|
@ -51,14 +51,16 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract {
|
|||
private readonly client: SavedObjectsClientContract;
|
||||
private readonly spaceId: string;
|
||||
private readonly types: string[];
|
||||
private readonly getSpacesClient: Promise<SpacesClient>;
|
||||
private readonly spacesClient: ISpacesClient;
|
||||
public readonly errors: SavedObjectsClientContract['errors'];
|
||||
|
||||
constructor(options: SpacesSavedObjectsClientOptions) {
|
||||
const { baseClient, request, spacesService, typeRegistry } = options;
|
||||
const { baseClient, request, getSpacesService, typeRegistry } = options;
|
||||
|
||||
const spacesService = getSpacesService();
|
||||
|
||||
this.client = baseClient;
|
||||
this.getSpacesClient = spacesService.scopedClient(request);
|
||||
this.spacesClient = spacesService.createSpacesClient(request);
|
||||
this.spaceId = spacesService.getSpaceId(request);
|
||||
this.types = typeRegistry.getAllTypes().map((t) => t.name);
|
||||
this.errors = baseClient.errors;
|
||||
|
@ -167,10 +169,8 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract {
|
|||
|
||||
let namespaces = options.namespaces;
|
||||
if (namespaces) {
|
||||
const spacesClient = await this.getSpacesClient;
|
||||
|
||||
try {
|
||||
const availableSpaces = await spacesClient.getAll({ purpose: 'findSavedObjects' });
|
||||
const availableSpaces = await this.spacesClient.getAll({ purpose: 'findSavedObjects' });
|
||||
if (namespaces.includes(ALL_SPACES_ID)) {
|
||||
namespaces = availableSpaces.map((space) => space.id);
|
||||
} else {
|
||||
|
|
14
x-pack/plugins/spaces/server/spaces_client/index.ts
Normal file
14
x-pack/plugins/spaces/server/spaces_client/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { SpacesClient, ISpacesClient } from './spaces_client';
|
||||
export {
|
||||
SpacesClientService,
|
||||
SpacesClientServiceSetup,
|
||||
SpacesClientServiceStart,
|
||||
SpacesClientRepositoryFactory,
|
||||
SpacesClientWrapper,
|
||||
} from './spaces_client_service';
|
|
@ -4,8 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { DEFAULT_SPACE_ID } from '../../../common/constants';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { DEFAULT_SPACE_ID } from '../../common/constants';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { SpacesClient } from './spaces_client';
|
||||
|
||||
const createSpacesClientMock = () =>
|
341
x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts
Normal file
341
x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts
Normal file
|
@ -0,0 +1,341 @@
|
|||
/*
|
||||
* 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 { SpacesClient } from './spaces_client';
|
||||
import { ConfigType, ConfigSchema } from '../config';
|
||||
import { GetAllSpacesPurpose } from '../../common/model/types';
|
||||
import { savedObjectsRepositoryMock } from '../../../../../src/core/server/mocks';
|
||||
|
||||
const createMockDebugLogger = () => {
|
||||
return jest.fn();
|
||||
};
|
||||
|
||||
const createMockConfig = (mockConfig: ConfigType = { maxSpaces: 1000, enabled: true }) => {
|
||||
return ConfigSchema.validate(mockConfig);
|
||||
};
|
||||
|
||||
describe('#getAll', () => {
|
||||
const savedObjects = [
|
||||
{
|
||||
id: 'foo',
|
||||
attributes: {
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
attributes: {
|
||||
name: 'bar-name',
|
||||
description: 'bar-description',
|
||||
bar: 'bar-bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
attributes: {
|
||||
name: 'baz-name',
|
||||
description: 'baz-description',
|
||||
bar: 'baz-bar',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const expectedSpaces = [
|
||||
{
|
||||
id: 'foo',
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
name: 'bar-name',
|
||||
description: 'bar-description',
|
||||
bar: 'bar-bar',
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
name: 'baz-name',
|
||||
description: 'baz-description',
|
||||
bar: 'baz-bar',
|
||||
},
|
||||
];
|
||||
|
||||
test(`finds spaces using callWithRequestRepository`, async () => {
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||
mockCallWithRequestRepository.find.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
} as any);
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces: 1234,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository);
|
||||
const actualSpaces = await client.getAll();
|
||||
|
||||
expect(actualSpaces).toEqual(expectedSpaces);
|
||||
expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: mockConfig.maxSpaces,
|
||||
sortField: 'name.keyword',
|
||||
});
|
||||
});
|
||||
|
||||
test(`throws Boom.badRequest when an invalid purpose is provided'`, async () => {
|
||||
const client = new SpacesClient(null as any, null as any, null as any);
|
||||
await expect(
|
||||
client.getAll({ purpose: 'invalid_purpose' as GetAllSpacesPurpose })
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"unsupported space purpose: invalid_purpose"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get', () => {
|
||||
const savedObject = {
|
||||
id: 'foo',
|
||||
type: 'foo',
|
||||
references: [],
|
||||
attributes: {
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
},
|
||||
};
|
||||
|
||||
const expectedSpace = {
|
||||
id: 'foo',
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
};
|
||||
|
||||
test(`gets space using callWithRequestRepository`, async () => {
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const mockConfig = createMockConfig();
|
||||
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
|
||||
|
||||
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository);
|
||||
const id = savedObject.id;
|
||||
const actualSpace = await client.get(id);
|
||||
|
||||
expect(actualSpace).toEqual(expectedSpace);
|
||||
expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#create', () => {
|
||||
const id = 'foo';
|
||||
|
||||
const spaceToCreate = {
|
||||
id,
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
_reserved: true,
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
const attributes = {
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
const savedObject = {
|
||||
id,
|
||||
type: 'foo',
|
||||
references: [],
|
||||
attributes: {
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
};
|
||||
|
||||
const expectedReturnedSpace = {
|
||||
id,
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
test(`creates space using callWithRequestRepository when we're under the max`, async () => {
|
||||
const maxSpaces = 5;
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||
mockCallWithRequestRepository.create.mockResolvedValue(savedObject);
|
||||
mockCallWithRequestRepository.find.mockResolvedValue({
|
||||
total: maxSpaces - 1,
|
||||
} as any);
|
||||
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository);
|
||||
|
||||
const actualSpace = await client.create(spaceToCreate);
|
||||
|
||||
expect(actualSpace).toEqual(expectedReturnedSpace);
|
||||
expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
});
|
||||
expect(mockCallWithRequestRepository.create).toHaveBeenCalledWith('space', attributes, {
|
||||
id,
|
||||
});
|
||||
});
|
||||
|
||||
test(`throws bad request when we are at the maximum number of spaces`, async () => {
|
||||
const maxSpaces = 5;
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||
mockCallWithRequestRepository.create.mockResolvedValue(savedObject);
|
||||
mockCallWithRequestRepository.find.mockResolvedValue({
|
||||
total: maxSpaces,
|
||||
} as any);
|
||||
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository);
|
||||
|
||||
expect(client.create(spaceToCreate)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting"`
|
||||
);
|
||||
|
||||
expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
});
|
||||
expect(mockCallWithRequestRepository.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
const spaceToUpdate = {
|
||||
id: 'foo',
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
_reserved: false,
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
const attributes = {
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
const savedObject = {
|
||||
id: 'foo',
|
||||
type: 'foo',
|
||||
references: [],
|
||||
attributes: {
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
_reserved: true,
|
||||
disabledFeatures: [],
|
||||
},
|
||||
};
|
||||
|
||||
const expectedReturnedSpace = {
|
||||
id: 'foo',
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
_reserved: true,
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
test(`updates space using callWithRequestRepository`, async () => {
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const mockConfig = createMockConfig();
|
||||
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
|
||||
|
||||
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository);
|
||||
const id = savedObject.id;
|
||||
const actualSpace = await client.update(id, spaceToUpdate);
|
||||
|
||||
expect(actualSpace).toEqual(expectedReturnedSpace);
|
||||
expect(mockCallWithRequestRepository.update).toHaveBeenCalledWith('space', id, attributes);
|
||||
expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
const id = 'foo';
|
||||
|
||||
const reservedSavedObject = {
|
||||
id,
|
||||
type: 'foo',
|
||||
references: [],
|
||||
attributes: {
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
_reserved: true,
|
||||
},
|
||||
};
|
||||
|
||||
const notReservedSavedObject = {
|
||||
id,
|
||||
type: 'foo',
|
||||
references: [],
|
||||
attributes: {
|
||||
name: 'foo-name',
|
||||
description: 'foo-description',
|
||||
bar: 'foo-bar',
|
||||
},
|
||||
};
|
||||
|
||||
test(`throws bad request when the space is reserved`, async () => {
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const mockConfig = createMockConfig();
|
||||
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||
mockCallWithRequestRepository.get.mockResolvedValue(reservedSavedObject);
|
||||
|
||||
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository);
|
||||
|
||||
expect(client.delete(id)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"The foo space cannot be deleted because it is reserved."`
|
||||
);
|
||||
|
||||
expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id);
|
||||
});
|
||||
|
||||
test(`deletes space using callWithRequestRepository when space isn't reserved`, async () => {
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const mockConfig = createMockConfig();
|
||||
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
|
||||
mockCallWithRequestRepository.get.mockResolvedValue(notReservedSavedObject);
|
||||
|
||||
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository);
|
||||
|
||||
await client.delete(id);
|
||||
|
||||
expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id);
|
||||
expect(mockCallWithRequestRepository.delete).toHaveBeenCalledWith('space', id);
|
||||
expect(mockCallWithRequestRepository.deleteByNamespace).toHaveBeenCalledWith(id);
|
||||
});
|
||||
});
|
110
x-pack/plugins/spaces/server/spaces_client/spaces_client.ts
Normal file
110
x-pack/plugins/spaces/server/spaces_client/spaces_client.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { omit } from 'lodash';
|
||||
import { ISavedObjectsRepository, SavedObject } from 'src/core/server';
|
||||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { isReservedSpace } from '../../common';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { ConfigType } from '../config';
|
||||
import { GetAllSpacesPurpose, GetSpaceResult } from '../../common/model/types';
|
||||
|
||||
export interface GetAllSpacesOptions {
|
||||
purpose?: GetAllSpacesPurpose;
|
||||
includeAuthorizedPurposes?: boolean;
|
||||
}
|
||||
|
||||
const SUPPORTED_GET_SPACE_PURPOSES: GetAllSpacesPurpose[] = [
|
||||
'any',
|
||||
'copySavedObjectsIntoSpace',
|
||||
'findSavedObjects',
|
||||
'shareSavedObjectsIntoSpace',
|
||||
];
|
||||
const DEFAULT_PURPOSE = 'any';
|
||||
|
||||
export type ISpacesClient = PublicMethodsOf<SpacesClient>;
|
||||
|
||||
export class SpacesClient {
|
||||
constructor(
|
||||
private readonly debugLogger: (message: string) => void,
|
||||
private readonly config: ConfigType,
|
||||
private readonly repository: ISavedObjectsRepository
|
||||
) {}
|
||||
|
||||
public async getAll(options: GetAllSpacesOptions = {}): Promise<GetSpaceResult[]> {
|
||||
const { purpose = DEFAULT_PURPOSE } = options;
|
||||
if (!SUPPORTED_GET_SPACE_PURPOSES.includes(purpose)) {
|
||||
throw Boom.badRequest(`unsupported space purpose: ${purpose}`);
|
||||
}
|
||||
|
||||
this.debugLogger(`SpacesClient.getAll(). querying all spaces`);
|
||||
|
||||
const { saved_objects: savedObjects } = await this.repository.find({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: this.config.maxSpaces,
|
||||
sortField: 'name.keyword',
|
||||
});
|
||||
|
||||
this.debugLogger(`SpacesClient.getAll(). Found ${savedObjects.length} spaces.`);
|
||||
|
||||
return savedObjects.map(this.transformSavedObjectToSpace);
|
||||
}
|
||||
|
||||
public async get(id: string) {
|
||||
const savedObject = await this.repository.get('space', id);
|
||||
return this.transformSavedObjectToSpace(savedObject);
|
||||
}
|
||||
|
||||
public async create(space: Space) {
|
||||
const { total } = await this.repository.find({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
});
|
||||
if (total >= this.config.maxSpaces) {
|
||||
throw Boom.badRequest(
|
||||
'Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting'
|
||||
);
|
||||
}
|
||||
|
||||
this.debugLogger(`SpacesClient.create(), using RBAC. Attempting to create space`);
|
||||
|
||||
const attributes = omit(space, ['id', '_reserved']);
|
||||
const id = space.id;
|
||||
const createdSavedObject = await this.repository.create('space', attributes, { id });
|
||||
|
||||
this.debugLogger(`SpacesClient.create(), created space object`);
|
||||
|
||||
return this.transformSavedObjectToSpace(createdSavedObject);
|
||||
}
|
||||
|
||||
public async update(id: string, space: Space) {
|
||||
const attributes = omit(space, 'id', '_reserved');
|
||||
await this.repository.update('space', id, attributes);
|
||||
const updatedSavedObject = await this.repository.get('space', id);
|
||||
return this.transformSavedObjectToSpace(updatedSavedObject);
|
||||
}
|
||||
|
||||
public async delete(id: string) {
|
||||
const existingSavedObject = await this.repository.get('space', id);
|
||||
if (isReservedSpace(this.transformSavedObjectToSpace(existingSavedObject))) {
|
||||
throw Boom.badRequest(`The ${id} space cannot be deleted because it is reserved.`);
|
||||
}
|
||||
|
||||
await this.repository.deleteByNamespace(id);
|
||||
|
||||
await this.repository.delete('space', id);
|
||||
}
|
||||
|
||||
private transformSavedObjectToSpace(savedObject: SavedObject<any>) {
|
||||
return {
|
||||
id: savedObject.id,
|
||||
...savedObject.attributes,
|
||||
} as Space;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { spacesClientMock } from '../mocks';
|
||||
|
||||
import { SpacesClientServiceSetup, SpacesClientServiceStart } from './spaces_client_service';
|
||||
|
||||
const createSpacesClientServiceSetupMock = () =>
|
||||
({
|
||||
registerClientWrapper: jest.fn(),
|
||||
setClientRepositoryFactory: jest.fn(),
|
||||
} as jest.Mocked<SpacesClientServiceSetup>);
|
||||
|
||||
const createSpacesClientServiceStartMock = () =>
|
||||
({
|
||||
createSpacesClient: jest.fn().mockReturnValue(spacesClientMock.create()),
|
||||
} as jest.Mocked<SpacesClientServiceStart>);
|
||||
|
||||
export const spacesClientServiceMock = {
|
||||
createSetup: createSpacesClientServiceSetupMock,
|
||||
createStart: createSpacesClientServiceStartMock,
|
||||
};
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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 * as Rx from 'rxjs';
|
||||
import { coreMock, httpServerMock } from 'src/core/server/mocks';
|
||||
import { ConfigType } from '../config';
|
||||
import { spacesConfig } from '../lib/__fixtures__';
|
||||
import { ISpacesClient, SpacesClient } from './spaces_client';
|
||||
import { SpacesClientService } from './spaces_client_service';
|
||||
|
||||
const debugLogger = jest.fn();
|
||||
|
||||
describe('SpacesClientService', () => {
|
||||
describe('#setup', () => {
|
||||
it('allows a single repository factory to be set', () => {
|
||||
const service = new SpacesClientService(debugLogger);
|
||||
const setup = service.setup({ config$: Rx.of(spacesConfig) });
|
||||
|
||||
const repositoryFactory = jest.fn();
|
||||
setup.setClientRepositoryFactory(repositoryFactory);
|
||||
|
||||
expect(() =>
|
||||
setup.setClientRepositoryFactory(repositoryFactory)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Repository factory has already been set"`);
|
||||
});
|
||||
|
||||
it('allows a single client wrapper to be set', () => {
|
||||
const service = new SpacesClientService(debugLogger);
|
||||
const setup = service.setup({ config$: Rx.of(spacesConfig) });
|
||||
|
||||
const clientWrapper = jest.fn();
|
||||
setup.registerClientWrapper(clientWrapper);
|
||||
|
||||
expect(() => setup.registerClientWrapper(clientWrapper)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Client wrapper has already been set"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#start', () => {
|
||||
it('throws if config is not available', () => {
|
||||
const service = new SpacesClientService(debugLogger);
|
||||
service.setup({ config$: new Rx.Observable<ConfigType>() });
|
||||
const coreStart = coreMock.createStart();
|
||||
const start = service.start(coreStart);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
expect(() => start.createSpacesClient(request)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Initialization error: spaces config is not available"`
|
||||
);
|
||||
});
|
||||
|
||||
describe('without a custom repository factory or wrapper', () => {
|
||||
it('returns an instance of the spaces client using the scoped repository', () => {
|
||||
const service = new SpacesClientService(debugLogger);
|
||||
service.setup({ config$: Rx.of(spacesConfig) });
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const start = service.start(coreStart);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const client = start.createSpacesClient(request);
|
||||
expect(client).toBeInstanceOf(SpacesClient);
|
||||
|
||||
expect(coreStart.savedObjects.createScopedRepository).toHaveBeenCalledWith(request, [
|
||||
'space',
|
||||
]);
|
||||
expect(coreStart.savedObjects.createInternalRepository).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the custom repository factory when set', () => {
|
||||
const service = new SpacesClientService(debugLogger);
|
||||
const setup = service.setup({ config$: Rx.of(spacesConfig) });
|
||||
|
||||
const customRepositoryFactory = jest.fn();
|
||||
setup.setClientRepositoryFactory(customRepositoryFactory);
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const start = service.start(coreStart);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const client = start.createSpacesClient(request);
|
||||
expect(client).toBeInstanceOf(SpacesClient);
|
||||
|
||||
expect(coreStart.savedObjects.createScopedRepository).not.toHaveBeenCalled();
|
||||
expect(coreStart.savedObjects.createInternalRepository).not.toHaveBeenCalled();
|
||||
|
||||
expect(customRepositoryFactory).toHaveBeenCalledWith(request, coreStart.savedObjects);
|
||||
});
|
||||
|
||||
it('wraps the client in the wrapper when registered', () => {
|
||||
const service = new SpacesClientService(debugLogger);
|
||||
const setup = service.setup({ config$: Rx.of(spacesConfig) });
|
||||
|
||||
const wrapper = (Symbol() as unknown) as ISpacesClient;
|
||||
|
||||
const clientWrapper = jest.fn().mockReturnValue(wrapper);
|
||||
setup.registerClientWrapper(clientWrapper);
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const start = service.start(coreStart);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const client = start.createSpacesClient(request);
|
||||
|
||||
expect(client).toBe(wrapper);
|
||||
expect(clientWrapper).toHaveBeenCalledTimes(1);
|
||||
expect(clientWrapper).toHaveBeenCalledWith(request, expect.any(SpacesClient));
|
||||
|
||||
expect(coreStart.savedObjects.createScopedRepository).toHaveBeenCalledWith(request, [
|
||||
'space',
|
||||
]);
|
||||
expect(coreStart.savedObjects.createInternalRepository).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('wraps the client in the wrapper when registered, using the custom repository factory when configured', () => {
|
||||
const service = new SpacesClientService(debugLogger);
|
||||
const setup = service.setup({ config$: Rx.of(spacesConfig) });
|
||||
|
||||
const customRepositoryFactory = jest.fn();
|
||||
setup.setClientRepositoryFactory(customRepositoryFactory);
|
||||
|
||||
const wrapper = (Symbol() as unknown) as ISpacesClient;
|
||||
|
||||
const clientWrapper = jest.fn().mockReturnValue(wrapper);
|
||||
setup.registerClientWrapper(clientWrapper);
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const start = service.start(coreStart);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const client = start.createSpacesClient(request);
|
||||
|
||||
expect(client).toBe(wrapper);
|
||||
expect(clientWrapper).toHaveBeenCalledTimes(1);
|
||||
expect(clientWrapper).toHaveBeenCalledWith(request, expect.any(SpacesClient));
|
||||
|
||||
expect(coreStart.savedObjects.createScopedRepository).not.toHaveBeenCalled();
|
||||
expect(coreStart.savedObjects.createInternalRepository).not.toHaveBeenCalled();
|
||||
|
||||
expect(customRepositoryFactory).toHaveBeenCalledWith(request, coreStart.savedObjects);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { Observable } from 'rxjs';
|
||||
import {
|
||||
KibanaRequest,
|
||||
CoreStart,
|
||||
ISavedObjectsRepository,
|
||||
SavedObjectsServiceStart,
|
||||
} from 'src/core/server';
|
||||
import { ConfigType } from '../config';
|
||||
import { SpacesClient, ISpacesClient } from './spaces_client';
|
||||
|
||||
export type SpacesClientWrapper = (
|
||||
request: KibanaRequest,
|
||||
baseClient: ISpacesClient
|
||||
) => ISpacesClient;
|
||||
|
||||
export type SpacesClientRepositoryFactory = (
|
||||
request: KibanaRequest,
|
||||
savedObjectsStart: SavedObjectsServiceStart
|
||||
) => ISavedObjectsRepository;
|
||||
|
||||
export interface SpacesClientServiceSetup {
|
||||
/**
|
||||
* Sets the factory that should be used to create the Saved Objects Repository
|
||||
* whenever a new instance of the SpacesClient is created. By default, a repository
|
||||
* scoped to the current user will be created.
|
||||
*/
|
||||
setClientRepositoryFactory: (factory: SpacesClientRepositoryFactory) => void;
|
||||
|
||||
/**
|
||||
* Sets the client wrapper that should be used to optionally "wrap" each instance of the SpacesClient.
|
||||
* By default, an unwrapped client will be created.
|
||||
*
|
||||
* Unlike the SavedObjectsClientWrappers, this service only supports a single wrapper. It is not possible
|
||||
* to register multiple wrappers at this time.
|
||||
*/
|
||||
registerClientWrapper: (wrapper: SpacesClientWrapper) => void;
|
||||
}
|
||||
|
||||
export interface SpacesClientServiceStart {
|
||||
/**
|
||||
* Creates an instance of the SpacesClient scoped to the provided request.
|
||||
*/
|
||||
createSpacesClient: (request: KibanaRequest) => ISpacesClient;
|
||||
}
|
||||
|
||||
interface SetupDeps {
|
||||
config$: Observable<ConfigType>;
|
||||
}
|
||||
|
||||
export class SpacesClientService {
|
||||
private repositoryFactory?: SpacesClientRepositoryFactory;
|
||||
|
||||
private config?: ConfigType;
|
||||
|
||||
private clientWrapper?: SpacesClientWrapper;
|
||||
|
||||
constructor(private readonly debugLogger: (message: string) => void) {}
|
||||
|
||||
public setup({ config$ }: SetupDeps): SpacesClientServiceSetup {
|
||||
config$.subscribe((nextConfig) => {
|
||||
this.config = nextConfig;
|
||||
});
|
||||
|
||||
return {
|
||||
setClientRepositoryFactory: (repositoryFactory: SpacesClientRepositoryFactory) => {
|
||||
if (this.repositoryFactory) {
|
||||
throw new Error(`Repository factory has already been set`);
|
||||
}
|
||||
this.repositoryFactory = repositoryFactory;
|
||||
},
|
||||
registerClientWrapper: (wrapper: SpacesClientWrapper) => {
|
||||
if (this.clientWrapper) {
|
||||
throw new Error(`Client wrapper has already been set`);
|
||||
}
|
||||
this.clientWrapper = wrapper;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start(coreStart: CoreStart): SpacesClientServiceStart {
|
||||
if (!this.repositoryFactory) {
|
||||
this.repositoryFactory = (request, savedObjectsStart) =>
|
||||
savedObjectsStart.createScopedRepository(request, ['space']);
|
||||
}
|
||||
return {
|
||||
createSpacesClient: (request: KibanaRequest) => {
|
||||
if (!this.config) {
|
||||
throw new Error('Initialization error: spaces config is not available');
|
||||
}
|
||||
|
||||
const baseClient = new SpacesClient(
|
||||
this.debugLogger,
|
||||
this.config,
|
||||
this.repositoryFactory!(request, coreStart.savedObjects)
|
||||
);
|
||||
if (this.clientWrapper) {
|
||||
return this.clientWrapper(request, baseClient);
|
||||
}
|
||||
return baseClient;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { SpacesService, SpacesServiceSetup } from './spaces_service';
|
||||
export { SpacesService, SpacesServiceSetup, SpacesServiceStart } from './spaces_service';
|
||||
|
|
|
@ -4,24 +4,33 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SpacesServiceSetup } from './spaces_service';
|
||||
import { spacesClientMock } from '../lib/spaces_client/spaces_client.mock';
|
||||
import { SpacesServiceSetup, SpacesServiceStart } from './spaces_service';
|
||||
import { spacesClientMock } from '../spaces_client/spaces_client.mock';
|
||||
import { DEFAULT_SPACE_ID } from '../../common/constants';
|
||||
import { namespaceToSpaceId, spaceIdToNamespace } from '../lib/utils/namespace';
|
||||
|
||||
const createSetupContractMock = (spaceId = DEFAULT_SPACE_ID) => {
|
||||
const setupContract: jest.Mocked<SpacesServiceSetup> = {
|
||||
getSpaceId: jest.fn().mockReturnValue(spaceId),
|
||||
isInDefaultSpace: jest.fn().mockReturnValue(spaceId === DEFAULT_SPACE_ID),
|
||||
getBasePath: jest.fn().mockReturnValue(''),
|
||||
scopedClient: jest.fn().mockResolvedValue(spacesClientMock.create()),
|
||||
namespaceToSpaceId: jest.fn().mockImplementation(namespaceToSpaceId),
|
||||
spaceIdToNamespace: jest.fn().mockImplementation(spaceIdToNamespace),
|
||||
getActiveSpace: jest.fn(),
|
||||
getSpaceId: jest.fn().mockReturnValue(spaceId),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
const createStartContractMock = (spaceId = DEFAULT_SPACE_ID) => {
|
||||
const startContract: jest.Mocked<SpacesServiceStart> = {
|
||||
namespaceToSpaceId: jest.fn().mockImplementation(namespaceToSpaceId),
|
||||
spaceIdToNamespace: jest.fn().mockImplementation(spaceIdToNamespace),
|
||||
createSpacesClient: jest.fn().mockReturnValue(spacesClientMock.create()),
|
||||
getSpaceId: jest.fn().mockReturnValue(spaceId),
|
||||
isInDefaultSpace: jest.fn().mockReturnValue(spaceId === DEFAULT_SPACE_ID),
|
||||
getActiveSpace: jest.fn(),
|
||||
};
|
||||
return startContract;
|
||||
};
|
||||
|
||||
export const spacesServiceMock = {
|
||||
createSetupContract: createSetupContractMock,
|
||||
createStartContract: createStartContractMock,
|
||||
};
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
*/
|
||||
import * as Rx from 'rxjs';
|
||||
import { SpacesService } from './spaces_service';
|
||||
import { coreMock, httpServerMock, loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { SpacesAuditLogger } from '../lib/audit_logger';
|
||||
import { coreMock, httpServerMock } from 'src/core/server/mocks';
|
||||
import {
|
||||
KibanaRequest,
|
||||
SavedObjectsErrorHelpers,
|
||||
|
@ -16,12 +15,10 @@ import {
|
|||
import { DEFAULT_SPACE_ID } from '../../common/constants';
|
||||
import { getSpaceIdFromPath } from '../../common/lib/spaces_url_parser';
|
||||
import { spacesConfig } from '../lib/__fixtures__';
|
||||
import { securityMock } from '../../../security/server/mocks';
|
||||
import { SpacesClientService } from '../spaces_client';
|
||||
|
||||
const mockLogger = loggingSystemMock.createLogger();
|
||||
|
||||
const createService = async (serverBasePath: string = '') => {
|
||||
const spacesService = new SpacesService(mockLogger);
|
||||
const createService = (serverBasePath: string = '') => {
|
||||
const spacesService = new SpacesService();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
|
@ -66,117 +63,95 @@ const createService = async (serverBasePath: string = '') => {
|
|||
return '/';
|
||||
});
|
||||
|
||||
const spacesServiceSetup = await spacesService.setup({
|
||||
http: httpSetup,
|
||||
getStartServices: async () => [coreStart, {}, {}],
|
||||
config$: Rx.of(spacesConfig),
|
||||
authorization: securityMock.createSetup().authz,
|
||||
auditLogger: new SpacesAuditLogger(),
|
||||
coreStart.http.basePath = httpSetup.basePath;
|
||||
|
||||
const spacesServiceSetup = spacesService.setup({
|
||||
basePath: httpSetup.basePath,
|
||||
});
|
||||
|
||||
return spacesServiceSetup;
|
||||
const spacesClientService = new SpacesClientService(jest.fn());
|
||||
spacesClientService.setup({
|
||||
config$: Rx.of(spacesConfig),
|
||||
});
|
||||
|
||||
const spacesClientServiceStart = spacesClientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = spacesService.start({
|
||||
basePath: coreStart.http.basePath,
|
||||
spacesClientService: spacesClientServiceStart,
|
||||
});
|
||||
|
||||
return {
|
||||
spacesServiceSetup,
|
||||
spacesServiceStart,
|
||||
};
|
||||
};
|
||||
|
||||
describe('SpacesService', () => {
|
||||
describe('#getSpaceId', () => {
|
||||
it('returns the default space id when no identifier is present', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
const { spacesServiceStart } = createService();
|
||||
|
||||
const request: KibanaRequest = {
|
||||
url: { pathname: '/app/kibana' },
|
||||
} as KibanaRequest;
|
||||
|
||||
expect(spacesServiceSetup.getSpaceId(request)).toEqual(DEFAULT_SPACE_ID);
|
||||
expect(spacesServiceStart.getSpaceId(request)).toEqual(DEFAULT_SPACE_ID);
|
||||
});
|
||||
|
||||
it('returns the space id when identifier is present', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
const { spacesServiceStart } = createService();
|
||||
|
||||
const request: KibanaRequest = {
|
||||
url: { pathname: '/s/foo/app/kibana' },
|
||||
} as KibanaRequest;
|
||||
|
||||
expect(spacesServiceSetup.getSpaceId(request)).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getBasePath', () => {
|
||||
it(`throws when a space id is not provided`, async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
|
||||
// @ts-ignore TS knows this isn't right
|
||||
expect(() => spacesServiceSetup.getBasePath()).toThrowErrorMatchingInlineSnapshot(
|
||||
`"spaceId is required to retrieve base path"`
|
||||
);
|
||||
|
||||
expect(() => spacesServiceSetup.getBasePath('')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"spaceId is required to retrieve base path"`
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "" for the default space and no server base path', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
expect(spacesServiceSetup.getBasePath(DEFAULT_SPACE_ID)).toEqual('');
|
||||
});
|
||||
|
||||
it('returns /sbp for the default space and the "/sbp" server base path', async () => {
|
||||
const spacesServiceSetup = await createService('/sbp');
|
||||
expect(spacesServiceSetup.getBasePath(DEFAULT_SPACE_ID)).toEqual('/sbp');
|
||||
});
|
||||
|
||||
it('returns /s/foo for the foo space and no server base path', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
expect(spacesServiceSetup.getBasePath('foo')).toEqual('/s/foo');
|
||||
});
|
||||
|
||||
it('returns /sbp/s/foo for the foo space and the "/sbp" server base path', async () => {
|
||||
const spacesServiceSetup = await createService('/sbp');
|
||||
expect(spacesServiceSetup.getBasePath('foo')).toEqual('/sbp/s/foo');
|
||||
expect(spacesServiceStart.getSpaceId(request)).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isInDefaultSpace', () => {
|
||||
it('returns true when in the default space', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
const { spacesServiceStart } = createService();
|
||||
|
||||
const request: KibanaRequest = {
|
||||
url: { pathname: '/app/kibana' },
|
||||
} as KibanaRequest;
|
||||
|
||||
expect(spacesServiceSetup.isInDefaultSpace(request)).toEqual(true);
|
||||
expect(spacesServiceStart.isInDefaultSpace(request)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when not in the default space', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
const { spacesServiceStart } = createService();
|
||||
|
||||
const request: KibanaRequest = {
|
||||
url: { pathname: '/s/foo/app/kibana' },
|
||||
} as KibanaRequest;
|
||||
|
||||
expect(spacesServiceSetup.isInDefaultSpace(request)).toEqual(false);
|
||||
expect(spacesServiceStart.isInDefaultSpace(request)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#spaceIdToNamespace', () => {
|
||||
it('returns the namespace for the given space', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
const { spacesServiceSetup } = createService();
|
||||
expect(spacesServiceSetup.spaceIdToNamespace('foo')).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#namespaceToSpaceId', () => {
|
||||
it('returns the space id for the given namespace', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
const { spacesServiceSetup } = createService();
|
||||
expect(spacesServiceSetup.namespaceToSpaceId('foo')).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getActiveSpace', () => {
|
||||
it('returns the default space when in the default space', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
const { spacesServiceStart } = createService();
|
||||
const request = httpServerMock.createKibanaRequest({ path: 'app/kibana' });
|
||||
|
||||
const activeSpace = await spacesServiceSetup.getActiveSpace(request);
|
||||
const activeSpace = await spacesServiceStart.getActiveSpace(request);
|
||||
expect(activeSpace).toEqual({
|
||||
id: 'space:default',
|
||||
name: 'Default Space',
|
||||
|
@ -186,10 +161,10 @@ describe('SpacesService', () => {
|
|||
});
|
||||
|
||||
it('returns the space for the current (non-default) space', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
const { spacesServiceStart } = createService();
|
||||
const request = httpServerMock.createKibanaRequest({ path: '/s/foo/app/kibana' });
|
||||
|
||||
const activeSpace = await spacesServiceSetup.getActiveSpace(request);
|
||||
const activeSpace = await spacesServiceStart.getActiveSpace(request);
|
||||
expect(activeSpace).toEqual({
|
||||
id: 'space:foo',
|
||||
name: 'Foo Space',
|
||||
|
@ -198,11 +173,11 @@ describe('SpacesService', () => {
|
|||
});
|
||||
|
||||
it('propagates errors from the repository', async () => {
|
||||
const spacesServiceSetup = await createService();
|
||||
const { spacesServiceStart } = createService();
|
||||
const request = httpServerMock.createKibanaRequest({ path: '/s/unknown-space/app/kibana' });
|
||||
|
||||
await expect(
|
||||
spacesServiceSetup.getActiveSpace(request)
|
||||
spacesServiceStart.getActiveSpace(request)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Saved object [space/unknown-space] not found"`
|
||||
);
|
||||
|
|
|
@ -4,133 +4,128 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Legacy } from 'kibana';
|
||||
import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server';
|
||||
import { SecurityPluginSetup } from '../../../security/server';
|
||||
import { SpacesClient } from '../lib/spaces_client';
|
||||
import { ConfigType } from '../config';
|
||||
import { getSpaceIdFromPath, addSpaceIdToPath } from '../../common/lib/spaces_url_parser';
|
||||
import type { KibanaRequest, IBasePath } from 'src/core/server';
|
||||
import { SpacesClientServiceStart } from '../spaces_client';
|
||||
import { getSpaceIdFromPath } from '../../common';
|
||||
import { DEFAULT_SPACE_ID } from '../../common/constants';
|
||||
import { spaceIdToNamespace, namespaceToSpaceId } from '../lib/utils/namespace';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { SpacesAuditLogger } from '../lib/audit_logger';
|
||||
|
||||
type RequestFacade = KibanaRequest | Legacy.Request;
|
||||
import { Space } from '..';
|
||||
|
||||
export interface SpacesServiceSetup {
|
||||
scopedClient(request: RequestFacade): Promise<SpacesClient>;
|
||||
|
||||
getSpaceId(request: RequestFacade): string;
|
||||
|
||||
getBasePath(spaceId: string): string;
|
||||
|
||||
isInDefaultSpace(request: RequestFacade): boolean;
|
||||
/**
|
||||
* Retrieves the space id associated with the provided request.
|
||||
* @param request
|
||||
*
|
||||
* @deprecated Use `getSpaceId` from the `SpacesServiceStart` contract instead.
|
||||
*/
|
||||
getSpaceId(request: KibanaRequest): string;
|
||||
|
||||
/**
|
||||
* Converts the provided space id into the corresponding Saved Objects `namespace` id.
|
||||
* @param spaceId
|
||||
*
|
||||
* @deprecated use `spaceIdToNamespace` from the `SpacesServiceStart` contract instead.
|
||||
*/
|
||||
spaceIdToNamespace(spaceId: string): string | undefined;
|
||||
|
||||
/**
|
||||
* Converts the provided namespace into the corresponding space id.
|
||||
* @param namespace
|
||||
*
|
||||
* @deprecated use `namespaceToSpaceId` from the `SpacesServiceStart` contract instead.
|
||||
*/
|
||||
namespaceToSpaceId(namespace: string | undefined): string;
|
||||
|
||||
getActiveSpace(request: RequestFacade): Promise<Space>;
|
||||
}
|
||||
|
||||
interface SpacesServiceDeps {
|
||||
http: CoreSetup['http'];
|
||||
getStartServices: CoreSetup['getStartServices'];
|
||||
authorization: SecurityPluginSetup['authz'] | null;
|
||||
config$: Observable<ConfigType>;
|
||||
auditLogger: SpacesAuditLogger;
|
||||
export interface SpacesServiceStart {
|
||||
/**
|
||||
* Creates a scoped instance of the SpacesClient.
|
||||
*/
|
||||
createSpacesClient: SpacesClientServiceStart['createSpacesClient'];
|
||||
|
||||
/**
|
||||
* Retrieves the space id associated with the provided request.
|
||||
* @param request
|
||||
*/
|
||||
getSpaceId(request: KibanaRequest): string;
|
||||
|
||||
/**
|
||||
* Indicates if the provided request is executing within the context of the `default` space.
|
||||
* @param request
|
||||
*/
|
||||
isInDefaultSpace(request: KibanaRequest): boolean;
|
||||
|
||||
/**
|
||||
* Retrieves the Space associated with the provided request.
|
||||
* @param request
|
||||
*/
|
||||
getActiveSpace(request: KibanaRequest): Promise<Space>;
|
||||
|
||||
/**
|
||||
* Converts the provided space id into the corresponding Saved Objects `namespace` id.
|
||||
* @param spaceId
|
||||
*/
|
||||
spaceIdToNamespace(spaceId: string): string | undefined;
|
||||
|
||||
/**
|
||||
* Converts the provided namespace into the corresponding space id.
|
||||
* @param namespace
|
||||
*/
|
||||
namespaceToSpaceId(namespace: string | undefined): string;
|
||||
}
|
||||
|
||||
interface SpacesServiceSetupDeps {
|
||||
basePath: IBasePath;
|
||||
}
|
||||
|
||||
interface SpacesServiceStartDeps {
|
||||
basePath: IBasePath;
|
||||
spacesClientService: SpacesClientServiceStart;
|
||||
}
|
||||
|
||||
export class SpacesService {
|
||||
private configSubscription$?: Subscription;
|
||||
|
||||
constructor(private readonly log: Logger) {}
|
||||
|
||||
public async setup({
|
||||
http,
|
||||
getStartServices,
|
||||
authorization,
|
||||
config$,
|
||||
auditLogger,
|
||||
}: SpacesServiceDeps): Promise<SpacesServiceSetup> {
|
||||
const getSpaceId = (request: RequestFacade) => {
|
||||
// Currently utilized by reporting
|
||||
const isFakeRequest = typeof (request as any).getBasePath === 'function';
|
||||
|
||||
const basePath = isFakeRequest
|
||||
? (request as Record<string, any>).getBasePath()
|
||||
: http.basePath.get(request);
|
||||
|
||||
const { spaceId } = getSpaceIdFromPath(basePath, http.basePath.serverBasePath);
|
||||
|
||||
return spaceId;
|
||||
};
|
||||
|
||||
const internalRepositoryPromise = getStartServices().then(([coreStart]) =>
|
||||
coreStart.savedObjects.createInternalRepository(['space'])
|
||||
);
|
||||
|
||||
const getScopedClient = async (request: KibanaRequest) => {
|
||||
const [coreStart] = await getStartServices();
|
||||
const internalRepository = await internalRepositoryPromise;
|
||||
|
||||
return config$
|
||||
.pipe(
|
||||
take(1),
|
||||
map((config) => {
|
||||
const callWithRequestRepository = coreStart.savedObjects.createScopedRepository(
|
||||
request,
|
||||
['space']
|
||||
);
|
||||
|
||||
return new SpacesClient(
|
||||
auditLogger,
|
||||
(message: string) => {
|
||||
this.log.debug(message);
|
||||
},
|
||||
authorization,
|
||||
callWithRequestRepository,
|
||||
config,
|
||||
internalRepository,
|
||||
request
|
||||
);
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
};
|
||||
|
||||
public setup({ basePath }: SpacesServiceSetupDeps): SpacesServiceSetup {
|
||||
return {
|
||||
getSpaceId,
|
||||
getBasePath: (spaceId: string) => {
|
||||
if (!spaceId) {
|
||||
throw new TypeError(`spaceId is required to retrieve base path`);
|
||||
}
|
||||
return addSpaceIdToPath(http.basePath.serverBasePath, spaceId);
|
||||
},
|
||||
isInDefaultSpace: (request: RequestFacade) => {
|
||||
const spaceId = getSpaceId(request);
|
||||
|
||||
return spaceId === DEFAULT_SPACE_ID;
|
||||
getSpaceId: (request: KibanaRequest) => {
|
||||
return this.getSpaceId(request, basePath);
|
||||
},
|
||||
spaceIdToNamespace,
|
||||
namespaceToSpaceId,
|
||||
scopedClient: getScopedClient,
|
||||
getActiveSpace: async (request: RequestFacade) => {
|
||||
const spaceId = getSpaceId(request);
|
||||
const spacesClient = await getScopedClient(
|
||||
request instanceof KibanaRequest ? request : KibanaRequest.from(request)
|
||||
);
|
||||
return spacesClient.get(spaceId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
if (this.configSubscription$) {
|
||||
this.configSubscription$.unsubscribe();
|
||||
this.configSubscription$ = undefined;
|
||||
}
|
||||
public start({ basePath, spacesClientService }: SpacesServiceStartDeps) {
|
||||
return {
|
||||
getSpaceId: (request: KibanaRequest) => {
|
||||
return this.getSpaceId(request, basePath);
|
||||
},
|
||||
|
||||
getActiveSpace: (request: KibanaRequest) => {
|
||||
const spaceId = this.getSpaceId(request, basePath);
|
||||
return spacesClientService.createSpacesClient(request).get(spaceId);
|
||||
},
|
||||
|
||||
isInDefaultSpace: (request: KibanaRequest) => {
|
||||
const spaceId = this.getSpaceId(request, basePath);
|
||||
|
||||
return spaceId === DEFAULT_SPACE_ID;
|
||||
},
|
||||
|
||||
createSpacesClient: (request: KibanaRequest) =>
|
||||
spacesClientService.createSpacesClient(request),
|
||||
|
||||
spaceIdToNamespace,
|
||||
namespaceToSpaceId,
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
||||
private getSpaceId(request: KibanaRequest, basePathService: IBasePath) {
|
||||
const basePath = basePathService.get(request);
|
||||
|
||||
const { spaceId } = getSpaceIdFromPath(basePath, basePathService.serverBasePath);
|
||||
|
||||
return spaceId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,7 +167,7 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
|
|||
expect(resp.body).to.eql({
|
||||
error: 'Bad Request',
|
||||
statusCode: 400,
|
||||
message: `This Space cannot be deleted because it is reserved.`,
|
||||
message: `The default space cannot be deleted because it is reserved.`,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue