[Alerting] Hides the action and action_task_params SavedObjects types (#67109)

As part of the work towards adding RBAC & Feature Controls support in Alerting (https://github.com/elastic/kibana/issues/43994), we've decided that the ActionsClient will handle authorisation against Actions instead of relying on the SavedObjectsClient on its own.

To prevent (or at least, minimise the chances of) bypassing this auth model by using the SavedObjects client this PR makes the `action` and `action_task_params` SavedObject types  _hidden_ types and given the ActionsClient permission to interact with it.
This commit is contained in:
Gidi Meir Morris 2020-05-22 09:07:09 +01:00 committed by GitHub
parent edee6543be
commit def6526384
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 50 additions and 28 deletions

View file

@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema';
import { ActionExecutor } from './action_executor';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
import { loggingServiceMock } from '../../../../../src/core/server/mocks';
import { loggingServiceMock, savedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { eventLoggerMock } from '../../../event_log/server/mocks';
import { spacesServiceMock } from '../../../spaces/server/spaces_service/spaces_service.mock';
import { ActionType } from '../types';
@ -17,7 +17,7 @@ import { actionsMock } from '../mocks';
const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false });
const services = actionsMock.createServices();
const savedObjectsClient = services.savedObjectsClient;
const savedObjectsClientWithHidden = savedObjectsClientMock.create();
const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
const actionTypeRegistry = actionTypeRegistryMock.create();
@ -34,6 +34,7 @@ actionExecutor.initialize({
logger: loggingServiceMock.create().get(),
spaces: spacesMock,
getServices: () => services,
getScopedSavedObjectsClient: () => savedObjectsClientWithHidden,
actionTypeRegistry,
encryptedSavedObjectsClient,
eventLogger: eventLoggerMock.create(),
@ -66,7 +67,7 @@ test('successfully executes', async () => {
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
await actionExecutor.execute(executeParams);
@ -107,7 +108,7 @@ test('provides empty config when config and / or secrets is empty', async () =>
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
await actionExecutor.execute(executeParams);
@ -137,7 +138,7 @@ test('throws an error when config is invalid', async () => {
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
@ -170,7 +171,7 @@ test('throws an error when params is invalid', async () => {
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
@ -184,7 +185,7 @@ test('throws an error when params is invalid', async () => {
});
test('throws an error when failing to load action through savedObjectsClient', async () => {
savedObjectsClient.get.mockRejectedValueOnce(new Error('No access'));
savedObjectsClientWithHidden.get.mockRejectedValueOnce(new Error('No access'));
await expect(actionExecutor.execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot(
`"No access"`
);
@ -205,7 +206,7 @@ test('throws an error if actionType is not enabled', async () => {
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => {
@ -239,7 +240,7 @@ test('should not throws an error if actionType is preconfigured', async () => {
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => {
@ -267,6 +268,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o
customActionExecutor.initialize({
logger: loggingServiceMock.create().get(),
spaces: spacesMock,
getScopedSavedObjectsClient: () => savedObjectsClientWithHidden,
getServices: () => services,
actionTypeRegistry,
encryptedSavedObjectsClient,

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Logger, KibanaRequest } from '../../../../../src/core/server';
import { Logger, KibanaRequest, SavedObjectsClientContract } from '../../../../../src/core/server';
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
import {
ActionTypeExecutorResult,
@ -12,7 +12,6 @@ import {
GetServicesFunction,
RawAction,
PreConfiguredAction,
Services,
} from '../types';
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
import { SpacesServiceSetup } from '../../../spaces/server';
@ -23,6 +22,7 @@ export interface ActionExecutorContext {
logger: Logger;
spaces?: SpacesServiceSetup;
getServices: GetServicesFunction;
getScopedSavedObjectsClient: (req: KibanaRequest) => SavedObjectsClientContract;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
actionTypeRegistry: ActionTypeRegistryContract;
eventLogger: IEventLogger;
@ -76,6 +76,7 @@ export class ActionExecutor {
actionTypeRegistry,
eventLogger,
preconfiguredActions,
getScopedSavedObjectsClient,
} = this.actionExecutorContext!;
const services = getServices(request);
@ -83,7 +84,7 @@ export class ActionExecutor {
const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {};
const { actionTypeId, name, config, secrets } = await getActionInfo(
services,
getScopedSavedObjectsClient(request),
encryptedSavedObjectsClient,
preconfiguredActions,
actionId,
@ -195,7 +196,7 @@ interface ActionInfo {
}
async function getActionInfo(
services: Services,
savedObjectsClient: SavedObjectsClientContract,
encryptedSavedObjectsClient: EncryptedSavedObjectsClient,
preconfiguredActions: PreConfiguredAction[],
actionId: string,
@ -218,7 +219,7 @@ async function getActionInfo(
// ensure user can read the action before processing
const {
attributes: { actionTypeId, config, name },
} = await services.savedObjectsClient.get<RawAction>('action', actionId);
} = await savedObjectsClient.get<RawAction>('action', actionId);
const {
attributes: { secrets },

View file

@ -59,6 +59,7 @@ const actionExecutorInitializerParams = {
logger: loggingServiceMock.create().get(),
getServices: jest.fn().mockReturnValue(services),
actionTypeRegistry,
getScopedSavedObjectsClient: () => savedObjectsClientMock.create(),
encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient,
eventLogger: eventLoggerMock.create(),
preconfiguredActions: [],

View file

@ -16,9 +16,9 @@ import {
SharedGlobalConfig,
RequestHandler,
IContextProvider,
SavedObjectsServiceStart,
ElasticsearchServiceStart,
IClusterClient,
SavedObjectsClientContract,
} from '../../../../src/core/server';
import {
@ -86,6 +86,8 @@ export interface ActionsPluginsStart {
taskManager: TaskManagerStartContract;
}
const includedHiddenTypes = ['action', 'action_task_params'];
export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, PluginStartContract> {
private readonly kibanaIndex: Promise<string>;
private readonly config: Promise<ActionsConfig>;
@ -190,7 +192,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
core.http.registerRouteHandlerContext(
'actions',
this.createRouteHandlerContext(await this.kibanaIndex)
this.createRouteHandlerContext(core, await this.kibanaIndex)
);
// Routes
@ -229,13 +231,27 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
preconfiguredActions,
} = this;
const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient();
const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({
includedHiddenTypes,
});
const getScopedSavedObjectsClient = (request: KibanaRequest) =>
core.savedObjects.getScopedClient(request, {
includedHiddenTypes,
});
const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) =>
core.savedObjects.getScopedClient(request);
actionExecutor!.initialize({
logger,
eventLogger: this.eventLogger!,
spaces: this.spaces,
getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch),
getScopedSavedObjectsClient,
getServices: this.getServicesFactory(
getScopedSavedObjectsClientWithoutAccessToActions,
core.elasticsearch
),
encryptedSavedObjectsClient,
actionTypeRegistry: actionTypeRegistry!,
preconfiguredActions,
@ -247,7 +263,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
encryptedSavedObjectsClient,
getBasePath: this.getBasePath,
spaceIdToNamespace: this.spaceIdToNamespace,
getScopedSavedObjectsClient: core.savedObjects.getScopedClient,
getScopedSavedObjectsClient,
});
scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager);
@ -256,7 +272,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
execute: createExecuteFunction({
taskManager: plugins.taskManager,
actionTypeRegistry: actionTypeRegistry!,
getScopedSavedObjectsClient: core.savedObjects.getScopedClient,
getScopedSavedObjectsClient,
getBasePath: this.getBasePath,
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
preconfiguredActions,
@ -275,7 +291,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
);
}
return new ActionsClient({
savedObjectsClient: core.savedObjects.getScopedClient(request),
savedObjectsClient: getScopedSavedObjectsClient(request),
actionTypeRegistry: actionTypeRegistry!,
defaultKibanaIndex: await kibanaIndex,
scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request),
@ -287,12 +303,12 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
}
private getServicesFactory(
savedObjects: SavedObjectsServiceStart,
getScopedClient: (request: KibanaRequest) => SavedObjectsClientContract,
elasticsearch: ElasticsearchServiceStart
): (request: KibanaRequest) => Services {
return (request) => ({
callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser,
savedObjectsClient: savedObjects.getScopedClient(request),
savedObjectsClient: getScopedClient(request),
getScopedCallCluster(clusterClient: IClusterClient) {
return clusterClient.asScoped(request).callAsCurrentUser;
},
@ -300,11 +316,13 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
}
private createRouteHandlerContext = (
core: CoreSetup<ActionsPluginsStart>,
defaultKibanaIndex: string
): IContextProvider<RequestHandler<unknown, unknown, unknown>, 'actions'> => {
const { actionTypeRegistry, isESOUsingEphemeralEncryptionKey, preconfiguredActions } = this;
return async function actionsRouteHandlerContext(context, request) {
const [{ savedObjects }] = await core.getStartServices();
return {
getActionsClient: () => {
if (isESOUsingEphemeralEncryptionKey === true) {
@ -313,7 +331,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
);
}
return new ActionsClient({
savedObjectsClient: context.core.savedObjects.client,
savedObjectsClient: savedObjects.getScopedClient(request, { includedHiddenTypes }),
actionTypeRegistry: actionTypeRegistry!,
defaultKibanaIndex,
scopedClusterClient: context.core.elasticsearch.adminClient,

View file

@ -14,7 +14,7 @@ export function setupSavedObjects(
) {
savedObjects.registerType({
name: 'action',
hidden: false,
hidden: true,
namespaceType: 'single',
mappings: mappings.action,
});
@ -31,7 +31,7 @@ export function setupSavedObjects(
savedObjects.registerType({
name: 'action_task_params',
hidden: false,
hidden: true,
namespaceType: 'single',
mappings: mappings.action_task_params,
});

View file

@ -291,7 +291,7 @@ export class AlertingPlugin {
savedObjects: SavedObjectsServiceStart,
request: KibanaRequest
) {
return savedObjects.getScopedClient(request, { includedHiddenTypes: ['alert'] });
return savedObjects.getScopedClient(request, { includedHiddenTypes: ['alert', 'action'] });
}
public stop() {

View file

@ -49,7 +49,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
const [, { encryptedSavedObjects }] = await core.getStartServices();
await encryptedSavedObjects
.getClient({
includedHiddenTypes: ['alert'],
includedHiddenTypes: ['alert', 'action'],
})
.getDecryptedAsInternalUser(req.body.type, req.body.id, {
namespace,