[Alerting] Hides the alert SavedObjects type (#66719)

* make alert saved object type hidden

* fix support for hidden alert type in alerting tests

* updated api docs

* fixed some missing types and unused imports

* fixed test broken by field rename

* added support for including hidden types in saved objects client

* fixed merge conflict

* cleaned up some test descriptions

* adds a getClient api to Encrypted Saved Objects

* fixed alerts fixture

* added missing plugin type in alerting

* removed unused field

* chaged ESO api to an options object as per Security teams request

* fixed usage of eso client

* fixed typos and oversights

* split alerts file into two - for actions and alerts
This commit is contained in:
Gidi Meir Morris 2020-05-21 11:00:15 +01:00 committed by GitHub
parent 6ad8c91894
commit 65370c70d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 648 additions and 507 deletions

View file

@ -88,8 +88,8 @@ export interface AlertingPluginsSetup {
}
export interface AlertingPluginsStart {
actions: ActionsPluginStartContract;
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
taskManager: TaskManagerStartContract;
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
}
export class AlertingPlugin {
@ -126,6 +126,7 @@ export class AlertingPlugin {
this.licenseState = new LicenseState(plugins.licensing.license$);
this.spaces = plugins.spaces?.spacesService;
this.security = plugins.security;
this.isESOUsingEphemeralEncryptionKey =
plugins.encryptedSavedObjects.usingEphemeralEncryptionKey;
@ -164,7 +165,7 @@ export class AlertingPlugin {
});
}
core.http.registerRouteHandlerContext('alerting', this.createRouteHandlerContext());
core.http.registerRouteHandlerContext('alerting', this.createRouteHandlerContext(core));
// Routes
const router = core.http.createRouter();
@ -201,7 +202,9 @@ export class AlertingPlugin {
security,
} = this;
const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient();
const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({
includedHiddenTypes: ['alert'],
});
alertsClientFactory.initialize({
alertTypeRegistry: alertTypeRegistry!,
@ -231,26 +234,32 @@ export class AlertingPlugin {
return {
listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!),
// Ability to get an alerts client from legacy code
getAlertsClientWithRequest(request: KibanaRequest) {
getAlertsClientWithRequest: (request: KibanaRequest) => {
if (isESOUsingEphemeralEncryptionKey === true) {
throw new Error(
`Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
);
}
return alertsClientFactory!.create(request, core.savedObjects.getScopedClient(request));
return alertsClientFactory!.create(
request,
this.getScopedClientWithAlertSavedObjectType(core.savedObjects, request)
);
},
};
}
private createRouteHandlerContext = (): IContextProvider<
RequestHandler<unknown, unknown, unknown>,
'alerting'
> => {
private createRouteHandlerContext = (
core: CoreSetup
): IContextProvider<RequestHandler<unknown, unknown, unknown>, 'alerting'> => {
const { alertTypeRegistry, alertsClientFactory } = this;
return async function alertsRouteHandlerContext(context, request) {
return async (context, request) => {
const [{ savedObjects }] = await core.getStartServices();
return {
getAlertsClient: () => {
return alertsClientFactory!.create(request, context.core.savedObjects.client);
return alertsClientFactory!.create(
request,
this.getScopedClientWithAlertSavedObjectType(savedObjects, request)
);
},
listTypes: alertTypeRegistry!.list.bind(alertTypeRegistry!),
};
@ -263,7 +272,7 @@ export class AlertingPlugin {
): (request: KibanaRequest) => Services {
return request => ({
callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser,
savedObjectsClient: savedObjects.getScopedClient(request),
savedObjectsClient: this.getScopedClientWithAlertSavedObjectType(savedObjects, request),
getScopedCallCluster(clusterClient: IClusterClient) {
return clusterClient.asScoped(request).callAsCurrentUser;
},
@ -278,6 +287,13 @@ export class AlertingPlugin {
return this.spaces && spaceId ? this.spaces.getBasePath(spaceId) : this.serverBasePath!;
};
private getScopedClientWithAlertSavedObjectType(
savedObjects: SavedObjectsServiceStart,
request: KibanaRequest
) {
return savedObjects.getScopedClient(request, { includedHiddenTypes: ['alert'] });
}
public stop() {
if (this.licenseState) {
this.licenseState.clean();

View file

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

View file

@ -68,7 +68,19 @@ router.get(
...
```
5. To retrieve Saved Object with decrypted content use the dedicated `getDecryptedAsInternalUser` API method.
5. Instantiate an EncryptedSavedObjects client so that you can interact with Saved Objects whose content has been encrypted.
```typescript
const esoClient = encryptedSavedObjects.getClient();
```
If your SavedObject type is a _hidden_ type, then you will have to specify it as an included type:
```typescript
const esoClient = encryptedSavedObjects.getClient({ includedHiddenTypes: ['myHiddenType'] });
```
6. To retrieve Saved Object with decrypted content use the dedicated `getDecryptedAsInternalUser` API method.
**Note:** As name suggests the method will retrieve the encrypted values and decrypt them on behalf of the internal Kibana
user to make it possible to use this method even when user request context is not available (e.g. in background tasks).
@ -77,7 +89,7 @@ and preferably only as a part of the Kibana server routines that are outside of
user has control over.
```typescript
const savedObjectWithDecryptedContent = await encryptedSavedObjects.getDecryptedAsInternalUser(
const savedObjectWithDecryptedContent = await esoClient.getDecryptedAsInternalUser(
'my-saved-object-type',
'saved-object-id'
);

View file

@ -5,7 +5,7 @@
*/
import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin';
import { EncryptedSavedObjectsClient } from './saved_objects';
import { EncryptedSavedObjectsClient, EncryptedSavedObjectsClientOptions } from './saved_objects';
function createEncryptedSavedObjectsSetupMock() {
return {
@ -18,11 +18,11 @@ function createEncryptedSavedObjectsSetupMock() {
function createEncryptedSavedObjectsStartMock() {
return {
isEncryptionError: jest.fn(),
getClient: jest.fn(() => createEncryptedSavedObjectsClienttMock()),
getClient: jest.fn(opts => createEncryptedSavedObjectsClienttMock(opts)),
} as jest.Mocked<EncryptedSavedObjectsPluginStart>;
}
function createEncryptedSavedObjectsClienttMock() {
function createEncryptedSavedObjectsClienttMock(opts?: EncryptedSavedObjectsClientOptions) {
return {
getDecryptedAsInternalUser: jest.fn(),
} as jest.Mocked<EncryptedSavedObjectsClient>;

View file

@ -14,7 +14,7 @@ import {
EncryptionError,
} from './crypto';
import { EncryptedSavedObjectsAuditLogger } from './audit';
import { SavedObjectsSetup, setupSavedObjects } from './saved_objects';
import { setupSavedObjects, ClientInstanciator } from './saved_objects';
export interface PluginsSetup {
security?: SecurityPluginSetup;
@ -28,7 +28,7 @@ export interface EncryptedSavedObjectsPluginSetup {
export interface EncryptedSavedObjectsPluginStart {
isEncryptionError: (error: Error) => boolean;
getClient: SavedObjectsSetup;
getClient: ClientInstanciator;
}
/**
@ -46,7 +46,7 @@ export interface LegacyAPI {
*/
export class Plugin {
private readonly logger: Logger;
private savedObjectsSetup!: SavedObjectsSetup;
private savedObjectsSetup!: ClientInstanciator;
private legacyAPI?: LegacyAPI;
private readonly getLegacyAPI = () => {
@ -95,7 +95,7 @@ export class Plugin {
this.logger.debug('Starting plugin');
return {
isEncryptionError: (error: Error) => error instanceof EncryptionError,
getClient: (includedHiddenTypes?: string[]) => this.savedObjectsSetup(includedHiddenTypes),
getClient: (options = {}) => this.savedObjectsSetup(options),
};
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectsSetup, setupSavedObjects } from '.';
import { ClientInstanciator, setupSavedObjects } from '.';
import {
coreMock,
@ -24,7 +24,7 @@ import {
import { EncryptedSavedObjectsService } from '../crypto';
describe('#setupSavedObjects', () => {
let setupContract: SavedObjectsSetup;
let setupContract: ClientInstanciator;
let coreStartMock: ReturnType<typeof coreMock.createStart>;
let coreSetupMock: ReturnType<typeof coreMock.createSetup>;
let mockSavedObjectsRepository: jest.Mocked<ISavedObjectsRepository>;
@ -91,7 +91,7 @@ describe('#setupSavedObjects', () => {
describe('#setupContract', () => {
it('includes hiddenTypes when specified', async () => {
await setupContract(['hiddenType']);
await setupContract({ includedHiddenTypes: ['hiddenType'] });
expect(coreStartMock.savedObjects.createInternalRepository).toHaveBeenCalledWith([
'hiddenType',
]);

View file

@ -23,7 +23,13 @@ interface SetupSavedObjectsParams {
getStartServices: StartServicesAccessor;
}
export type SavedObjectsSetup = (includedHiddenTypes?: string[]) => EncryptedSavedObjectsClient;
export type ClientInstanciator = (
options?: EncryptedSavedObjectsClientOptions
) => EncryptedSavedObjectsClient;
export interface EncryptedSavedObjectsClientOptions {
includedHiddenTypes?: string[];
}
export interface EncryptedSavedObjectsClient {
getDecryptedAsInternalUser: <T = unknown>(
@ -38,7 +44,7 @@ export function setupSavedObjects({
savedObjects,
security,
getStartServices,
}: SetupSavedObjectsParams): SavedObjectsSetup {
}: SetupSavedObjectsParams): ClientInstanciator {
// Register custom saved object client that will encrypt, decrypt and strip saved object
// attributes where appropriate for any saved object repository request. We choose max possible
// priority for this wrapper to allow all other wrappers to set proper `namespace` for the Saved
@ -56,11 +62,11 @@ export function setupSavedObjects({
})
);
return (includedHiddenTypes?: string[]) => {
return clientOpts => {
const internalRepositoryAndTypeRegistryPromise = getStartServices().then(
([core]) =>
[
core.savedObjects.createInternalRepository(includedHiddenTypes),
core.savedObjects.createInternalRepository(clientOpts?.includedHiddenTypes),
core.savedObjects.getTypeRegistry(),
] as [ISavedObjectsRepository, ISavedObjectTypeRegistry]
);

View file

@ -19,7 +19,6 @@ import { SpacesPluginSetup } from '../../../../../../../plugins/spaces/server';
interface FixtureSetupDeps {
spaces?: SpacesPluginSetup;
}
interface FixtureStartDeps {
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
}
@ -44,12 +43,14 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
): Promise<IKibanaResponse<any>> {
try {
let namespace: string | undefined;
const [, { encryptedSavedObjects }] = await core.getStartServices();
if (spaces && req.body.spaceId) {
namespace = spaces.spacesService.spaceIdToNamespace(req.body.spaceId);
}
const [, { encryptedSavedObjects }] = await core.getStartServices();
await encryptedSavedObjects
.getClient()
.getClient({
includedHiddenTypes: ['alert'],
})
.getDecryptedAsInternalUser(req.body.type, req.body.id, {
namespace,
});

View file

@ -0,0 +1,200 @@
/*
* 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 { CoreSetup } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { FixtureStartDeps, FixtureSetupDeps } from './plugin';
import { ActionType, ActionTypeExecutorOptions } from '../../../../../../../plugins/actions/server';
export function defineActionTypes(
core: CoreSetup<FixtureStartDeps>,
{ actions }: Pick<FixtureSetupDeps, 'actions'>
) {
const clusterClient = core.elasticsearch.adminClient;
// Action types
const noopActionType: ActionType = {
id: 'test.noop',
name: 'Test: Noop',
minimumLicenseRequired: 'gold',
async executor() {
return { status: 'ok', actionId: '' };
},
};
const indexRecordActionType: ActionType = {
id: 'test.index-record',
name: 'Test: Index Record',
minimumLicenseRequired: 'gold',
validate: {
params: schema.object({
index: schema.string(),
reference: schema.string(),
message: schema.string(),
}),
config: schema.object({
unencrypted: schema.string(),
}),
secrets: schema.object({
encrypted: schema.string(),
}),
},
async executor({ config, secrets, params, services, actionId }: ActionTypeExecutorOptions) {
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
params,
config,
secrets,
reference: params.reference,
source: 'action:test.index-record',
},
});
return { status: 'ok', actionId };
},
};
const failingActionType: ActionType = {
id: 'test.failing',
name: 'Test: Failing',
minimumLicenseRequired: 'gold',
validate: {
params: schema.object({
index: schema.string(),
reference: schema.string(),
}),
},
async executor({ config, secrets, params, services }: ActionTypeExecutorOptions) {
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
params,
config,
secrets,
reference: params.reference,
source: 'action:test.failing',
},
});
throw new Error(`expected failure for ${params.index} ${params.reference}`);
},
};
const rateLimitedActionType: ActionType = {
id: 'test.rate-limit',
name: 'Test: Rate Limit',
minimumLicenseRequired: 'gold',
maxAttempts: 2,
validate: {
params: schema.object({
index: schema.string(),
reference: schema.string(),
retryAt: schema.number(),
}),
},
async executor({ config, params, services }: ActionTypeExecutorOptions) {
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
params,
config,
reference: params.reference,
source: 'action:test.rate-limit',
},
});
return {
status: 'error',
retry: new Date(params.retryAt),
actionId: '',
};
},
};
const authorizationActionType: ActionType = {
id: 'test.authorization',
name: 'Test: Authorization',
minimumLicenseRequired: 'gold',
validate: {
params: schema.object({
callClusterAuthorizationIndex: schema.string(),
savedObjectsClientType: schema.string(),
savedObjectsClientId: schema.string(),
index: schema.string(),
reference: schema.string(),
}),
},
async executor({ params, services, actionId }: ActionTypeExecutorOptions) {
// Call cluster
let callClusterSuccess = false;
let callClusterError;
try {
await services.callCluster('index', {
index: params.callClusterAuthorizationIndex,
refresh: 'wait_for',
body: {
param1: 'test',
},
});
callClusterSuccess = true;
} catch (e) {
callClusterError = e;
}
// Call scoped cluster
const callScopedCluster = services.getScopedCallCluster(clusterClient);
let callScopedClusterSuccess = false;
let callScopedClusterError;
try {
await callScopedCluster('index', {
index: params.callClusterAuthorizationIndex,
refresh: 'wait_for',
body: {
param1: 'test',
},
});
callScopedClusterSuccess = true;
} catch (e) {
callScopedClusterError = e;
}
// Saved objects client
let savedObjectsClientSuccess = false;
let savedObjectsClientError;
try {
await services.savedObjectsClient.get(
params.savedObjectsClientType,
params.savedObjectsClientId
);
savedObjectsClientSuccess = true;
} catch (e) {
savedObjectsClientError = e;
}
// Save the result
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
state: {
callClusterSuccess,
callClusterError,
callScopedClusterSuccess,
callScopedClusterError,
savedObjectsClientSuccess,
savedObjectsClientError,
},
params,
reference: params.reference,
source: 'action:test.authorization',
},
});
return {
actionId,
status: 'ok',
};
},
};
actions.registerType(noopActionType);
actions.registerType(indexRecordActionType);
actions.registerType(failingActionType);
actions.registerType(rateLimitedActionType);
actions.registerType(authorizationActionType);
}

View file

@ -0,0 +1,298 @@
/*
* 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 { CoreSetup } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { times } from 'lodash';
import { FixtureStartDeps, FixtureSetupDeps } from './plugin';
import { AlertType, AlertExecutorOptions } from '../../../../../../../plugins/alerting/server';
export function defineAlertTypes(
core: CoreSetup<FixtureStartDeps>,
{ alerting }: Pick<FixtureSetupDeps, 'alerting'>
) {
const clusterClient = core.elasticsearch.adminClient;
const alwaysFiringAlertType: AlertType = {
id: 'test.always-firing',
name: 'Test: Always Firing',
actionGroups: [
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
producer: 'alerting',
defaultActionGroupId: 'default',
actionVariables: {
state: [{ name: 'instanceStateValue', description: 'the instance state value' }],
context: [{ name: 'instanceContextValue', description: 'the instance context value' }],
},
async executor(alertExecutorOptions: AlertExecutorOptions) {
const {
services,
params,
state,
alertId,
spaceId,
namespace,
name,
tags,
createdBy,
updatedBy,
} = alertExecutorOptions;
let group = 'default';
const alertInfo = { alertId, spaceId, namespace, name, tags, createdBy, updatedBy };
if (params.groupsToScheduleActionsInSeries) {
const index = state.groupInSeriesIndex || 0;
group = params.groupsToScheduleActionsInSeries[index];
}
if (group) {
services
.alertInstanceFactory('1')
.replaceState({ instanceStateValue: true })
.scheduleActions(group, {
instanceContextValue: true,
});
}
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
state,
params,
reference: params.reference,
source: 'alert:test.always-firing',
alertInfo,
},
});
return {
globalStateValue: true,
groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1,
};
},
};
// Alert types
const cumulativeFiringAlertType: AlertType = {
id: 'test.cumulative-firing',
name: 'Test: Cumulative Firing',
actionGroups: [
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor(alertExecutorOptions: AlertExecutorOptions) {
const { services, state } = alertExecutorOptions;
const group = 'default';
const runCount = (state.runCount || 0) + 1;
times(runCount, index => {
services
.alertInstanceFactory(`instance-${index}`)
.replaceState({ instanceStateValue: true })
.scheduleActions(group);
});
return {
runCount,
};
},
};
const neverFiringAlertType: AlertType = {
id: 'test.never-firing',
name: 'Test: Never firing',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor({ services, params, state }: AlertExecutorOptions) {
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
state,
params,
reference: params.reference,
source: 'alert:test.never-firing',
},
});
return {
globalStateValue: true,
};
},
};
const failingAlertType: AlertType = {
id: 'test.failing',
name: 'Test: Failing',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor({ services, params, state }: AlertExecutorOptions) {
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
state,
params,
reference: params.reference,
source: 'alert:test.failing',
},
});
throw new Error('Failed to execute alert type');
},
};
const authorizationAlertType: AlertType = {
id: 'test.authorization',
name: 'Test: Authorization',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
defaultActionGroupId: 'default',
producer: 'alerting',
validate: {
params: schema.object({
callClusterAuthorizationIndex: schema.string(),
savedObjectsClientType: schema.string(),
savedObjectsClientId: schema.string(),
index: schema.string(),
reference: schema.string(),
}),
},
async executor({ services, params, state }: AlertExecutorOptions) {
// Call cluster
let callClusterSuccess = false;
let callClusterError;
try {
await services.callCluster('index', {
index: params.callClusterAuthorizationIndex,
refresh: 'wait_for',
body: {
param1: 'test',
},
});
callClusterSuccess = true;
} catch (e) {
callClusterError = e;
}
// Call scoped cluster
const callScopedCluster = services.getScopedCallCluster(clusterClient);
let callScopedClusterSuccess = false;
let callScopedClusterError;
try {
await callScopedCluster('index', {
index: params.callClusterAuthorizationIndex,
refresh: 'wait_for',
body: {
param1: 'test',
},
});
callScopedClusterSuccess = true;
} catch (e) {
callScopedClusterError = e;
}
// Saved objects client
let savedObjectsClientSuccess = false;
let savedObjectsClientError;
try {
await services.savedObjectsClient.get(
params.savedObjectsClientType,
params.savedObjectsClientId
);
savedObjectsClientSuccess = true;
} catch (e) {
savedObjectsClientError = e;
}
// Save the result
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
state: {
callClusterSuccess,
callClusterError,
callScopedClusterSuccess,
callScopedClusterError,
savedObjectsClientSuccess,
savedObjectsClientError,
},
params,
reference: params.reference,
source: 'alert:test.authorization',
},
});
},
};
const validationAlertType: AlertType = {
id: 'test.validation',
name: 'Test: Validation',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
producer: 'alerting',
defaultActionGroupId: 'default',
validate: {
params: schema.object({
param1: schema.string(),
}),
},
async executor({ services, params, state }: AlertExecutorOptions) {},
};
const noopAlertType: AlertType = {
id: 'test.noop',
name: 'Test: Noop',
actionGroups: [{ id: 'default', name: 'Default' }],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor({ services, params, state }: AlertExecutorOptions) {},
};
const onlyContextVariablesAlertType: AlertType = {
id: 'test.onlyContextVariables',
name: 'Test: Only Context Variables',
actionGroups: [{ id: 'default', name: 'Default' }],
producer: 'alerting',
defaultActionGroupId: 'default',
actionVariables: {
context: [{ name: 'aContextVariable', description: 'this is a context variable' }],
},
async executor(opts: AlertExecutorOptions) {},
};
const onlyStateVariablesAlertType: AlertType = {
id: 'test.onlyStateVariables',
name: 'Test: Only State Variables',
actionGroups: [{ id: 'default', name: 'Default' }],
producer: 'alerting',
defaultActionGroupId: 'default',
actionVariables: {
state: [{ name: 'aStateVariable', description: 'this is a state variable' }],
},
async executor(opts: AlertExecutorOptions) {},
};
alerting.registerType(alwaysFiringAlertType);
alerting.registerType(cumulativeFiringAlertType);
alerting.registerType(neverFiringAlertType);
alerting.registerType(failingAlertType);
alerting.registerType(validationAlertType);
alerting.registerType(authorizationAlertType);
alerting.registerType(noopAlertType);
alerting.registerType(onlyContextVariablesAlertType);
alerting.registerType(onlyStateVariablesAlertType);
}

View file

@ -5,22 +5,21 @@
*/
import { Plugin, CoreSetup } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { times } from 'lodash';
import { PluginSetupContract as ActionsPluginSetup } from '../../../../../../../plugins/actions/server/plugin';
import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../../plugins/alerting/server/plugin';
import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server';
import { ActionType, ActionTypeExecutorOptions } from '../../../../../../../plugins/actions/server';
import { AlertType, AlertExecutorOptions } from '../../../../../../../plugins/alerting/server';
import { defineAlertTypes } from './alert_types';
import { defineActionTypes } from './action_types';
import { defineRoutes } from './routes';
interface FixtureSetupDeps {
export interface FixtureSetupDeps {
features: FeaturesPluginSetup;
actions: ActionsPluginSetup;
alerting: AlertingPluginSetup;
}
interface FixtureStartDeps {
export interface FixtureStartDeps {
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
}
@ -29,7 +28,6 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
core: CoreSetup<FixtureStartDeps>,
{ features, actions, alerting }: FixtureSetupDeps
) {
const clusterClient = core.elasticsearch.adminClient;
features.registerFeature({
id: 'alerting',
name: 'Alerting',
@ -55,469 +53,10 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
},
},
});
// Action types
const noopActionType: ActionType = {
id: 'test.noop',
name: 'Test: Noop',
minimumLicenseRequired: 'gold',
async executor() {
return { status: 'ok', actionId: '' };
},
};
const indexRecordActionType: ActionType = {
id: 'test.index-record',
name: 'Test: Index Record',
minimumLicenseRequired: 'gold',
validate: {
params: schema.object({
index: schema.string(),
reference: schema.string(),
message: schema.string(),
}),
config: schema.object({
unencrypted: schema.string(),
}),
secrets: schema.object({
encrypted: schema.string(),
}),
},
async executor({ config, secrets, params, services, actionId }: ActionTypeExecutorOptions) {
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
params,
config,
secrets,
reference: params.reference,
source: 'action:test.index-record',
},
});
return { status: 'ok', actionId };
},
};
const failingActionType: ActionType = {
id: 'test.failing',
name: 'Test: Failing',
minimumLicenseRequired: 'gold',
validate: {
params: schema.object({
index: schema.string(),
reference: schema.string(),
}),
},
async executor({ config, secrets, params, services }: ActionTypeExecutorOptions) {
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
params,
config,
secrets,
reference: params.reference,
source: 'action:test.failing',
},
});
throw new Error(`expected failure for ${params.index} ${params.reference}`);
},
};
const rateLimitedActionType: ActionType = {
id: 'test.rate-limit',
name: 'Test: Rate Limit',
minimumLicenseRequired: 'gold',
maxAttempts: 2,
validate: {
params: schema.object({
index: schema.string(),
reference: schema.string(),
retryAt: schema.number(),
}),
},
async executor({ config, params, services }: ActionTypeExecutorOptions) {
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
params,
config,
reference: params.reference,
source: 'action:test.rate-limit',
},
});
return {
status: 'error',
retry: new Date(params.retryAt),
actionId: '',
};
},
};
const authorizationActionType: ActionType = {
id: 'test.authorization',
name: 'Test: Authorization',
minimumLicenseRequired: 'gold',
validate: {
params: schema.object({
callClusterAuthorizationIndex: schema.string(),
savedObjectsClientType: schema.string(),
savedObjectsClientId: schema.string(),
index: schema.string(),
reference: schema.string(),
}),
},
async executor({ params, services, actionId }: ActionTypeExecutorOptions) {
// Call cluster
let callClusterSuccess = false;
let callClusterError;
try {
await services.callCluster('index', {
index: params.callClusterAuthorizationIndex,
refresh: 'wait_for',
body: {
param1: 'test',
},
});
callClusterSuccess = true;
} catch (e) {
callClusterError = e;
}
// Call scoped cluster
const callScopedCluster = services.getScopedCallCluster(clusterClient);
let callScopedClusterSuccess = false;
let callScopedClusterError;
try {
await callScopedCluster('index', {
index: params.callClusterAuthorizationIndex,
refresh: 'wait_for',
body: {
param1: 'test',
},
});
callScopedClusterSuccess = true;
} catch (e) {
callScopedClusterError = e;
}
// Saved objects client
let savedObjectsClientSuccess = false;
let savedObjectsClientError;
try {
await services.savedObjectsClient.get(
params.savedObjectsClientType,
params.savedObjectsClientId
);
savedObjectsClientSuccess = true;
} catch (e) {
savedObjectsClientError = e;
}
// Save the result
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
state: {
callClusterSuccess,
callClusterError,
callScopedClusterSuccess,
callScopedClusterError,
savedObjectsClientSuccess,
savedObjectsClientError,
},
params,
reference: params.reference,
source: 'action:test.authorization',
},
});
return {
actionId,
status: 'ok',
};
},
};
actions.registerType(noopActionType);
actions.registerType(indexRecordActionType);
actions.registerType(failingActionType);
actions.registerType(rateLimitedActionType);
actions.registerType(authorizationActionType);
const alwaysFiringAlertType: AlertType = {
id: 'test.always-firing',
name: 'Test: Always Firing',
actionGroups: [
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
producer: 'alerting',
defaultActionGroupId: 'default',
actionVariables: {
state: [{ name: 'instanceStateValue', description: 'the instance state value' }],
context: [{ name: 'instanceContextValue', description: 'the instance context value' }],
},
async executor(alertExecutorOptions: AlertExecutorOptions) {
const {
services,
params,
state,
alertId,
spaceId,
namespace,
name,
tags,
createdBy,
updatedBy,
} = alertExecutorOptions;
let group = 'default';
const alertInfo = { alertId, spaceId, namespace, name, tags, createdBy, updatedBy };
if (params.groupsToScheduleActionsInSeries) {
const index = state.groupInSeriesIndex || 0;
group = params.groupsToScheduleActionsInSeries[index];
}
if (group) {
services
.alertInstanceFactory('1')
.replaceState({ instanceStateValue: true })
.scheduleActions(group, {
instanceContextValue: true,
});
}
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
state,
params,
reference: params.reference,
source: 'alert:test.always-firing',
alertInfo,
},
});
return {
globalStateValue: true,
groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1,
};
},
};
// Alert types
const cumulativeFiringAlertType: AlertType = {
id: 'test.cumulative-firing',
name: 'Test: Cumulative Firing',
actionGroups: [
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor(alertExecutorOptions: AlertExecutorOptions) {
const { services, state } = alertExecutorOptions;
const group = 'default';
const runCount = (state.runCount || 0) + 1;
times(runCount, index => {
services
.alertInstanceFactory(`instance-${index}`)
.replaceState({ instanceStateValue: true })
.scheduleActions(group);
});
return {
runCount,
};
},
};
const neverFiringAlertType: AlertType = {
id: 'test.never-firing',
name: 'Test: Never firing',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor({ services, params, state }: AlertExecutorOptions) {
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
state,
params,
reference: params.reference,
source: 'alert:test.never-firing',
},
});
return {
globalStateValue: true,
};
},
};
const failingAlertType: AlertType = {
id: 'test.failing',
name: 'Test: Failing',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor({ services, params, state }: AlertExecutorOptions) {
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
state,
params,
reference: params.reference,
source: 'alert:test.failing',
},
});
throw new Error('Failed to execute alert type');
},
};
const authorizationAlertType: AlertType = {
id: 'test.authorization',
name: 'Test: Authorization',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
defaultActionGroupId: 'default',
producer: 'alerting',
validate: {
params: schema.object({
callClusterAuthorizationIndex: schema.string(),
savedObjectsClientType: schema.string(),
savedObjectsClientId: schema.string(),
index: schema.string(),
reference: schema.string(),
}),
},
async executor({ services, params, state }: AlertExecutorOptions) {
// Call cluster
let callClusterSuccess = false;
let callClusterError;
try {
await services.callCluster('index', {
index: params.callClusterAuthorizationIndex,
refresh: 'wait_for',
body: {
param1: 'test',
},
});
callClusterSuccess = true;
} catch (e) {
callClusterError = e;
}
// Call scoped cluster
const callScopedCluster = services.getScopedCallCluster(clusterClient);
let callScopedClusterSuccess = false;
let callScopedClusterError;
try {
await callScopedCluster('index', {
index: params.callClusterAuthorizationIndex,
refresh: 'wait_for',
body: {
param1: 'test',
},
});
callScopedClusterSuccess = true;
} catch (e) {
callScopedClusterError = e;
}
// Saved objects client
let savedObjectsClientSuccess = false;
let savedObjectsClientError;
try {
await services.savedObjectsClient.get(
params.savedObjectsClientType,
params.savedObjectsClientId
);
savedObjectsClientSuccess = true;
} catch (e) {
savedObjectsClientError = e;
}
// Save the result
await services.callCluster('index', {
index: params.index,
refresh: 'wait_for',
body: {
state: {
callClusterSuccess,
callClusterError,
callScopedClusterSuccess,
callScopedClusterError,
savedObjectsClientSuccess,
savedObjectsClientError,
},
params,
reference: params.reference,
source: 'alert:test.authorization',
},
});
},
};
const validationAlertType: AlertType = {
id: 'test.validation',
name: 'Test: Validation',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
producer: 'alerting',
defaultActionGroupId: 'default',
validate: {
params: schema.object({
param1: schema.string(),
}),
},
async executor({ services, params, state }: AlertExecutorOptions) {},
};
const noopAlertType: AlertType = {
id: 'test.noop',
name: 'Test: Noop',
actionGroups: [{ id: 'default', name: 'Default' }],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor({ services, params, state }: AlertExecutorOptions) {},
};
const onlyContextVariablesAlertType: AlertType = {
id: 'test.onlyContextVariables',
name: 'Test: Only Context Variables',
actionGroups: [{ id: 'default', name: 'Default' }],
producer: 'alerting',
defaultActionGroupId: 'default',
actionVariables: {
context: [{ name: 'aContextVariable', description: 'this is a context variable' }],
},
async executor(opts: AlertExecutorOptions) {},
};
const onlyStateVariablesAlertType: AlertType = {
id: 'test.onlyStateVariables',
name: 'Test: Only State Variables',
actionGroups: [{ id: 'default', name: 'Default' }],
producer: 'alerting',
defaultActionGroupId: 'default',
actionVariables: {
state: [{ name: 'aStateVariable', description: 'this is a state variable' }],
},
async executor(opts: AlertExecutorOptions) {},
};
alerting.registerType(alwaysFiringAlertType);
alerting.registerType(cumulativeFiringAlertType);
alerting.registerType(neverFiringAlertType);
alerting.registerType(failingAlertType);
alerting.registerType(validationAlertType);
alerting.registerType(authorizationAlertType);
alerting.registerType(noopAlertType);
alerting.registerType(onlyContextVariablesAlertType);
alerting.registerType(onlyStateVariablesAlertType);
defineActionTypes(core, { actions });
defineAlertTypes(core, { alerting });
defineRoutes(core);
}
public start() {}

View file

@ -0,0 +1,57 @@
/*
* 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 {
CoreSetup,
RequestHandlerContext,
KibanaRequest,
KibanaResponseFactory,
IKibanaResponse,
} from 'kibana/server';
import { schema } from '@kbn/config-schema';
export function defineRoutes(core: CoreSetup) {
const router = core.http.createRouter();
router.put(
{
path: '/api/alerts_fixture/saved_object/{type}/{id}',
validate: {
params: schema.object({
type: schema.string(),
id: schema.string(),
}),
body: schema.object({
attributes: schema.recordOf(schema.string(), schema.any()),
version: schema.maybe(schema.string()),
references: schema.maybe(
schema.arrayOf(
schema.object({
name: schema.string(),
type: schema.string(),
id: schema.string(),
})
)
),
}),
},
},
async (
context: RequestHandlerContext,
req: KibanaRequest<any, any, any, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> => {
const { type, id } = req.params;
const { attributes, version, references } = req.body;
const options = { version, references };
const [{ savedObjects }] = await core.getStartServices();
const savedObjectsWithAlerts = await savedObjects.getScopedClient(req, {
includedHiddenTypes: ['alert'],
});
const result = await savedObjectsWithAlerts.update(type, id, attributes, options);
return res.ok({ body: result });
}
);
}

View file

@ -117,7 +117,9 @@ export default function createDeleteTests({ getService }: FtrProviderContext) {
.expect(200);
await supertest
.put(`${getUrlPrefix(space.id)}/api/saved_objects/alert/${createdAlert.id}`)
.put(
`${getUrlPrefix(space.id)}/api/alerts_fixture/saved_object/alert/${createdAlert.id}`
)
.set('kbn-xsrf', 'foo')
.send({
attributes: {

View file

@ -93,7 +93,9 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
objectRemover.add(space.id, createdAlert.id, 'alert');
await supertest
.put(`${getUrlPrefix(space.id)}/api/saved_objects/alert/${createdAlert.id}`)
.put(
`${getUrlPrefix(space.id)}/api/alerts_fixture/saved_object/alert/${createdAlert.id}`
)
.set('kbn-xsrf', 'foo')
.send({
attributes: {

View file

@ -98,7 +98,9 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex
objectRemover.add(space.id, createdAlert.id, 'alert');
await supertest
.put(`${getUrlPrefix(space.id)}/api/saved_objects/alert/${createdAlert.id}`)
.put(
`${getUrlPrefix(space.id)}/api/alerts_fixture/saved_object/alert/${createdAlert.id}`
)
.set('kbn-xsrf', 'foo')
.send({
attributes: {

View file

@ -117,7 +117,9 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
objectRemover.add(space.id, createdAlert.id, 'alert');
await supertest
.put(`${getUrlPrefix(space.id)}/api/saved_objects/alert/${createdAlert.id}`)
.put(
`${getUrlPrefix(space.id)}/api/alerts_fixture/saved_object/alert/${createdAlert.id}`
)
.set('kbn-xsrf', 'foo')
.send({
attributes: {

View file

@ -83,7 +83,9 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte
objectRemover.add(space.id, createdAlert.id, 'alert');
await supertest
.put(`${getUrlPrefix(space.id)}/api/saved_objects/alert/${createdAlert.id}`)
.put(
`${getUrlPrefix(space.id)}/api/alerts_fixture/saved_object/alert/${createdAlert.id}`
)
.set('kbn-xsrf', 'foo')
.send({
attributes: {

View file

@ -26,7 +26,9 @@ export function registerHiddenSORoutes(
try {
return response.ok({
body: await encryptedSavedObjects
.getClient([request.params.type])
.getClient({
includedHiddenTypes: [request.params.type],
})
.getDecryptedAsInternalUser(request.params.type, request.params.id, { namespace }),
});
} catch (err) {