[Uptime] Generate api key for synthetics service (#119590) (#119758)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2021-11-25 21:54:55 +01:00 committed by GitHub
parent 227a9b35fa
commit 0ad7556dc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 365 additions and 42 deletions

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
export const SyntheticsServiceApiKeyType = t.type({
id: t.string,
name: t.string,
apiKey: t.string,
});
export const SyntheticsServiceApiKeySaveType = t.intersection([
t.type({
success: t.boolean,
}),
t.partial({
error: t.string,
}),
]);
export type SyntheticsServiceApiKey = t.TypeOf<typeof SyntheticsServiceApiKeyType>;
export type SyntheticsServiceApiKeySaveResponse = t.TypeOf<typeof SyntheticsServiceApiKeySaveType>;

View file

@ -14,13 +14,15 @@
"requiredPlugins": [
"alerting",
"embeddable",
"encryptedSavedObjects",
"inspector",
"features",
"licensing",
"triggersActionsUi",
"usageCollection",
"observability",
"ruleRegistry",
"observability"
"security",
"triggersActionsUi",
"usageCollection"
],
"server": true,
"ui": true,

View file

@ -11,7 +11,7 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PLUGIN } from '../common/constants/plugin';
import { compose } from './lib/compose/kibana';
import { initUptimeServer } from './uptime_server';
import { UptimeCorePlugins, UptimeCoreSetup } from './lib/adapters/framework';
import { UptimeCorePluginsSetup, UptimeCoreSetup } from './lib/adapters/framework';
import { umDynamicSettings } from './lib/saved_objects/uptime_settings';
import { UptimeRuleRegistry } from './plugin';
@ -29,7 +29,7 @@ export interface KibanaServer extends Server {
export const initServerWithKibana = (
server: UptimeCoreSetup,
plugins: UptimeCorePlugins,
plugins: UptimeCorePluginsSetup,
ruleRegistry: UptimeRuleRegistry,
logger: Logger
) => {

View file

@ -12,12 +12,17 @@ import type {
IScopedClusterClient,
} from 'src/core/server';
import { ObservabilityPluginSetup } from '../../../../../observability/server';
import {
EncryptedSavedObjectsPluginSetup,
EncryptedSavedObjectsPluginStart,
} from '../../../../../encrypted_saved_objects/server';
import { UMKibanaRoute } from '../../../rest_api';
import { PluginSetupContract } from '../../../../../features/server';
import { MlPluginSetup as MlSetup } from '../../../../../ml/server';
import { RuleRegistryPluginSetupContract } from '../../../../../rule_registry/server';
import { UptimeESClient } from '../../lib';
import type { UptimeRouter } from '../../../types';
import { SecurityPluginStart } from '../../../../../security/server';
import { UptimeConfig } from '../../../../common/config';
export type UMElasticsearchQueryFn<P, R = any> = (
@ -35,16 +40,23 @@ export type UMSavedObjectsQueryFn<T = any, P = undefined> = (
export interface UptimeCoreSetup {
router: UptimeRouter;
config: UptimeConfig;
security: SecurityPluginStart;
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
}
export interface UptimeCorePlugins {
export interface UptimeCorePluginsSetup {
features: PluginSetupContract;
alerting: any;
elasticsearch: any;
observability: ObservabilityPluginSetup;
usageCollection: UsageCollectionSetup;
ml: MlSetup;
ruleRegistry: RuleRegistryPluginSetupContract;
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
}
export interface UptimeCorePluginsStart {
security: SecurityPluginStart;
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
}
export interface UMBackendFrameworkAdapter {

View file

@ -19,7 +19,7 @@ import { DURATION_ANOMALY } from '../../../common/constants/alerts';
import { commonStateTranslations, durationAnomalyTranslations } from './translations';
import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies';
import { getSeverityType } from '../../../../ml/common/util/anomaly_utils';
import { UptimeCorePlugins } from '../adapters/framework';
import { UptimeCorePluginsSetup } from '../adapters/framework';
import { UptimeAlertTypeFactory } from './types';
import { Ping } from '../../../common/runtime_types/ping';
import { getMLJobId } from '../../../common/lib';
@ -45,7 +45,7 @@ export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Pi
};
const getAnomalies = async (
plugins: UptimeCorePlugins,
plugins: UptimeCorePluginsSetup,
savedObjectsClient: SavedObjectsClientContract,
params: Record<any, any>,
lastCheckedAt: string

View file

@ -7,7 +7,7 @@
import { Logger } from 'kibana/server';
import { UMServerLibs } from '../../lib';
import { UptimeCorePlugins, UptimeCoreSetup } from '../../adapters';
import { UptimeCorePluginsSetup, UptimeCoreSetup } from '../../adapters';
import type { UptimeRouter } from '../../../types';
import type { IRuleDataClient } from '../../../../../rule_registry/server';
import { ruleRegistryMocks } from '../../../../../rule_registry/server/mocks';
@ -27,8 +27,8 @@ export const bootstrapDependencies = (customRequests?: any, customPlugins: any =
const router = {} as UptimeRouter;
// these server/libs parameters don't have any functionality, which is fine
// because we aren't testing them here
const server: UptimeCoreSetup = { router, config: {} };
const plugins: UptimeCorePlugins = customPlugins as any;
const server = { router, config: {} } as UptimeCoreSetup;
const plugins: UptimeCorePluginsSetup = customPlugins as any;
const libs: UMServerLibs = { requests: {} } as UMServerLibs;
libs.requests = { ...libs.requests, ...customRequests };
return { server, libs, plugins };

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters';
import { UptimeCorePluginsSetup, UptimeCoreSetup } from '../adapters';
import { UMServerLibs } from '../lib';
import { AlertTypeWithExecutor } from '../../../../rule_registry/server';
import { AlertInstanceContext, AlertTypeState } from '../../../../alerting/common';
@ -32,5 +32,5 @@ export type DefaultUptimeAlertInstance<TActionGroupIds extends string> = AlertTy
export type UptimeAlertTypeFactory<TActionGroupIds extends string> = (
server: UptimeCoreSetup,
libs: UMServerLibs,
plugins: UptimeCorePlugins
plugins: UptimeCorePluginsSetup
) => DefaultUptimeAlertInstance<TActionGroupIds>;

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { savedObjectsAdapter } from './saved_objects';

View file

@ -9,12 +9,33 @@ import {
SavedObjectsErrorHelpers,
SavedObjectsServiceSetup,
} from '../../../../../../src/core/server';
import { EncryptedSavedObjectsPluginSetup } from '../../../../encrypted_saved_objects/server';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { DynamicSettings } from '../../../common/runtime_types';
import { UMSavedObjectsQueryFn } from '../adapters';
import { UptimeConfig } from '../../../common/config';
import { settingsObjectId, umDynamicSettings } from './uptime_settings';
import { syntheticsMonitor } from './synthetics_monitor';
import { syntheticsServiceApiKey } from './service_api_key';
export const registerUptimeSavedObjects = (
savedObjectsService: SavedObjectsServiceSetup,
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
config: UptimeConfig
) => {
savedObjectsService.registerType(umDynamicSettings);
if (config?.unsafe?.service.enabled) {
savedObjectsService.registerType(syntheticsMonitor);
savedObjectsService.registerType(syntheticsServiceApiKey);
encryptedSavedObjects.registerType({
type: syntheticsServiceApiKey.name,
attributesToEncrypt: new Set(['apiKey']),
});
}
};
export interface UMSavedObjectsAdapter {
config: UptimeConfig;
@ -22,20 +43,9 @@ export interface UMSavedObjectsAdapter {
setUptimeDynamicSettings: UMSavedObjectsQueryFn<void, DynamicSettings>;
}
export const registerUptimeSavedObjects = (
savedObjectsService: SavedObjectsServiceSetup,
config: UptimeConfig
) => {
savedObjectsService.registerType(umDynamicSettings);
if (config?.unsafe?.service.enabled) {
savedObjectsService.registerType(syntheticsMonitor);
}
};
export const savedObjectsAdapter: UMSavedObjectsAdapter = {
config: null,
getUptimeDynamicSettings: async (client): Promise<DynamicSettings> => {
getUptimeDynamicSettings: async (client) => {
try {
const obj = await client.get<DynamicSettings>(umDynamicSettings.name, settingsObjectId);
return obj?.attributes ?? DYNAMIC_SETTINGS_DEFAULTS;
@ -50,7 +60,7 @@ export const savedObjectsAdapter: UMSavedObjectsAdapter = {
throw getErr;
}
},
setUptimeDynamicSettings: async (client, settings): Promise<void> => {
setUptimeDynamicSettings: async (client, settings) => {
await client.create(umDynamicSettings.name, settings, {
id: settingsObjectId,
overwrite: true,

View file

@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import {
SavedObjectsClientContract,
SavedObjectsErrorHelpers,
SavedObjectsType,
} from '../../../../../../src/core/server';
import { SyntheticsServiceApiKey } from '../../../common/runtime_types/synthetics_service_api_key';
import { EncryptedSavedObjectsClient } from '../../../../encrypted_saved_objects/server';
export const syntheticsApiKeyID = 'ba997842-b0cf-4429-aa9d-578d9bf0d391';
const syntheticsApiKeyObjectType = 'uptime-synthetics-api-key';
export const syntheticsServiceApiKey: SavedObjectsType = {
name: syntheticsApiKeyObjectType,
hidden: true,
namespaceType: 'single',
mappings: {
dynamic: false,
properties: {
apiKey: {
type: 'binary',
},
/* Leaving these commented to make it clear that these fields exist, even though we don't want them indexed.
When adding new fields please add them here. If they need to be searchable put them in the uncommented
part of properties.
id: {
type: 'keyword',
},
name: {
type: 'long',
},
*/
},
},
management: {
importableAndExportable: false,
icon: 'uptimeApp',
getTitle: () =>
i18n.translate('xpack.uptime.synthetics.service.apiKey', {
defaultMessage: 'Synthetics service api key',
}),
},
};
export const getSyntheticsServiceAPIKey = async (client: EncryptedSavedObjectsClient) => {
try {
const obj = await client.getDecryptedAsInternalUser<SyntheticsServiceApiKey>(
syntheticsServiceApiKey.name,
syntheticsApiKeyID
);
return obj?.attributes;
} catch (getErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
return undefined;
}
throw getErr;
}
};
export const setSyntheticsServiceApiKey = async (
client: SavedObjectsClientContract,
apiKey: SyntheticsServiceApiKey
) => {
await client.create(syntheticsServiceApiKey.name, apiKey, {
id: syntheticsApiKeyID,
overwrite: true,
});
};

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getAPIKeyForSyntheticsService } from './get_api_key';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
import { securityMock } from '../../../../security/server/mocks';
import { coreMock } from '../../../../../../src/core/server/mocks';
import { syntheticsServiceApiKey } from '../saved_objects/service_api_key';
import { KibanaRequest } from 'kibana/server';
describe('getAPIKeyTest', function () {
const core = coreMock.createStart();
const security = securityMock.createStart();
const encryptedSavedObject = encryptedSavedObjectsMock.createStart();
const request = {} as KibanaRequest;
security.authc.apiKeys.areAPIKeysEnabled = jest.fn().mockReturnValue(true);
security.authc.apiKeys.create = jest.fn().mockReturnValue({
id: 'test',
name: 'service-api-key',
api_key: 'qwerty',
encoded: '@#$%^&',
});
it('should generate an api key and return it', async () => {
const apiKey = await getAPIKeyForSyntheticsService({
request,
security,
encryptedSavedObject,
savedObjectsClient: core.savedObjects.getScopedClient(request),
});
expect(security.authc.apiKeys.areAPIKeysEnabled).toHaveBeenCalledTimes(1);
expect(security.authc.apiKeys.create).toHaveBeenCalledTimes(1);
expect(security.authc.apiKeys.create).toHaveBeenCalledWith(
{},
{
name: 'synthetics-api-key',
role_descriptors: {
synthetics_writer: {
cluster: ['monitor', 'read_ilm', 'read_pipeline'],
index: [
{
names: ['synthetics-*'],
privileges: ['view_index_metadata', 'create_doc', 'auto_configure'],
},
],
},
},
metadata: {
description:
'Created for synthetics service to be passed to the heartbeat to communicate with ES',
},
}
);
expect(apiKey).toEqual({ apiKey: 'qwerty', id: 'test', name: 'service-api-key' });
});
it('should return existing api key', async () => {
const getObject = jest
.fn()
.mockReturnValue({ attributes: { apiKey: 'qwerty', id: 'test', name: 'service-api-key' } });
encryptedSavedObject.getClient = jest.fn().mockReturnValue({
getDecryptedAsInternalUser: getObject,
});
const apiKey = await getAPIKeyForSyntheticsService({
request,
security,
encryptedSavedObject,
savedObjectsClient: core.savedObjects.getScopedClient(request),
});
expect(apiKey).toEqual({ apiKey: 'qwerty', id: 'test', name: 'service-api-key' });
expect(encryptedSavedObject.getClient).toHaveBeenCalledTimes(1);
expect(getObject).toHaveBeenCalledTimes(1);
expect(encryptedSavedObject.getClient).toHaveBeenCalledWith({
includedHiddenTypes: [syntheticsServiceApiKey.name],
});
expect(getObject).toHaveBeenCalledWith(
'uptime-synthetics-api-key',
'ba997842-b0cf-4429-aa9d-578d9bf0d391'
);
});
});

View file

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server';
import { EncryptedSavedObjectsPluginStart } from '../../../../encrypted_saved_objects/server';
import { SecurityPluginStart } from '../../../../security/server';
import {
getSyntheticsServiceAPIKey,
setSyntheticsServiceApiKey,
syntheticsServiceApiKey,
} from '../saved_objects/service_api_key';
import { SyntheticsServiceApiKey } from '../../../common/runtime_types/synthetics_service_api_key';
export const getAPIKeyForSyntheticsService = async ({
encryptedSavedObject,
savedObjectsClient,
request,
security,
}: {
encryptedSavedObject: EncryptedSavedObjectsPluginStart;
request: KibanaRequest;
security: SecurityPluginStart;
savedObjectsClient: SavedObjectsClientContract;
}): Promise<SyntheticsServiceApiKey | Error | undefined> => {
const encryptedClient = encryptedSavedObject.getClient({
includedHiddenTypes: [syntheticsServiceApiKey.name],
});
const apiKey = await getSyntheticsServiceAPIKey(encryptedClient);
if (apiKey) {
return apiKey;
}
return await generateAndSaveAPIKey({ request, security, savedObjectsClient });
};
export const generateAndSaveAPIKey = async ({
security,
request,
savedObjectsClient,
}: {
security: SecurityPluginStart;
request: KibanaRequest;
savedObjectsClient: SavedObjectsClientContract;
}) => {
try {
const isApiKeysEnabled = await security.authc.apiKeys?.areAPIKeysEnabled();
if (!isApiKeysEnabled) {
return new Error('Please enable API keys in kibana to use synthetics service.');
}
const apiKeyResult = await security.authc.apiKeys?.create(request, {
name: 'synthetics-api-key',
role_descriptors: {
synthetics_writer: {
cluster: ['monitor', 'read_ilm', 'read_pipeline'],
index: [
{
names: ['synthetics-*'],
privileges: ['view_index_metadata', 'create_doc', 'auto_configure'],
},
],
},
},
metadata: {
description:
'Created for synthetics service to be passed to the heartbeat to communicate with ES',
},
});
if (apiKeyResult) {
const { id, name, api_key: apiKey } = apiKeyResult;
const apiKeyObject = { id, name, apiKey };
// discard decoded key and rest of the keys
await setSyntheticsServiceApiKey(savedObjectsClient, apiKeyObject);
return apiKeyObject;
}
} catch (e) {
throw e;
}
};

View file

@ -15,7 +15,12 @@ import {
} from '../../../../src/core/server';
import { uptimeRuleFieldMap } from '../common/rules/uptime_rule_field_map';
import { initServerWithKibana } from './kibana.index';
import { KibanaTelemetryAdapter, UptimeCorePlugins } from './lib/adapters';
import {
KibanaTelemetryAdapter,
UptimeCorePluginsSetup,
UptimeCorePluginsStart,
UptimeCoreSetup,
} from './lib/adapters';
import { registerUptimeSavedObjects, savedObjectsAdapter } from './lib/saved_objects/saved_objects';
import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map';
import { Dataset } from '../../rule_registry/server';
@ -27,12 +32,13 @@ export class Plugin implements PluginType {
private savedObjectsClient?: ISavedObjectsRepository;
private initContext: PluginInitializerContext;
private logger?: Logger;
private server?: UptimeCoreSetup;
constructor(_initializerContext: PluginInitializerContext<UptimeConfig>) {
this.initContext = _initializerContext;
}
public setup(core: CoreSetup, plugins: UptimeCorePlugins) {
public setup(core: CoreSetup, plugins: UptimeCorePluginsSetup) {
const config = this.initContext.config.get<UptimeConfig>();
savedObjectsAdapter.config = config;
@ -53,14 +59,11 @@ export class Plugin implements PluginType {
],
});
initServerWithKibana(
{ router: core.http.createRouter(), config },
plugins,
ruleDataClient,
this.logger
);
this.server = { router: core.http.createRouter(), config } as UptimeCoreSetup;
registerUptimeSavedObjects(core.savedObjects, config);
initServerWithKibana(this.server, plugins, ruleDataClient, this.logger);
registerUptimeSavedObjects(core.savedObjects, plugins.encryptedSavedObjects, config);
KibanaTelemetryAdapter.registerUsageCollector(
plugins.usageCollection,
@ -72,8 +75,12 @@ export class Plugin implements PluginType {
};
}
public start(core: CoreStart, _plugins: any) {
public start(core: CoreStart, plugins: UptimeCorePluginsStart) {
this.savedObjectsClient = core.savedObjects.createInternalRepository();
if (this.server) {
this.server.security = plugins.security;
this.server.encryptedSavedObjects = plugins.encryptedSavedObjects;
}
}
public stop() {}

View file

@ -20,6 +20,7 @@ export const createRouteWithAuth = (
request,
response,
savedObjectsClient,
server,
}) => {
const { statusCode, message } = libs.license(context.licensing.license);
if (statusCode === 200) {
@ -29,6 +30,7 @@ export const createRouteWithAuth = (
request,
response,
savedObjectsClient,
server,
});
}
switch (statusCode) {

View file

@ -17,6 +17,7 @@ import {
} from 'kibana/server';
import { UMServerLibs, UptimeESClient } from '../lib/lib';
import type { UptimeRequestHandlerContext } from '../types';
import { UptimeCoreSetup } from '../lib/adapters';
/**
* Defines the basic properties employed by Uptime routes.
@ -58,7 +59,10 @@ export type UMRestApiRouteFactory = (libs: UMServerLibs) => UptimeRoute;
* Functions of this type accept our internal route format and output a route
* object that the Kibana platform can consume.
*/
export type UMKibanaRouteWrapper = (uptimeRoute: UptimeRoute) => UMKibanaRoute;
export type UMKibanaRouteWrapper = (
uptimeRoute: UptimeRoute,
server: UptimeCoreSetup
) => UMKibanaRoute;
/**
* This is the contract we specify internally for route handling.
@ -68,6 +72,7 @@ export type UMRouteHandler = ({
context,
request,
response,
server,
savedObjectsClient,
}: {
uptimeEsClient: UptimeESClient;
@ -75,4 +80,5 @@ export type UMRouteHandler = ({
request: KibanaRequest<Record<string, any>, Record<string, any>, Record<string, any>>;
response: KibanaResponseFactory;
savedObjectsClient: SavedObjectsClientContract;
server: UptimeCoreSetup;
}) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;

View file

@ -12,7 +12,7 @@ import { createUptimeESClient, inspectableEsQueriesMap } from '../lib/lib';
import { KibanaResponse } from '../../../../../src/core/server/http/router';
import { enableInspectEsQueries } from '../../../observability/common';
export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({
export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => ({
...uptimeRoute,
options: {
tags: ['access:uptime-read', ...(uptimeRoute?.writeAccess ? ['access:uptime-write'] : [])],
@ -40,6 +40,7 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({
context,
request,
response,
server,
});
if (res instanceof KibanaResponse) {

View file

@ -9,7 +9,7 @@ import { Logger } from 'kibana/server';
import { createLifecycleRuleTypeFactory, IRuleDataClient } from '../../rule_registry/server';
import { UMServerLibs } from './lib/lib';
import { createRouteWithAuth, restApiRoutes, uptimeRouteWrapper } from './rest_api';
import { UptimeCoreSetup, UptimeCorePlugins } from './lib/adapters';
import { UptimeCoreSetup, UptimeCorePluginsSetup } from './lib/adapters';
import { statusCheckAlertFactory } from './lib/alerts/status_check';
import { tlsAlertFactory } from './lib/alerts/tls';
@ -19,12 +19,12 @@ import { durationAnomalyAlertFactory } from './lib/alerts/duration_anomaly';
export const initUptimeServer = (
server: UptimeCoreSetup,
libs: UMServerLibs,
plugins: UptimeCorePlugins,
plugins: UptimeCorePluginsSetup,
ruleDataClient: IRuleDataClient,
logger: Logger
) => {
restApiRoutes.forEach((route) =>
libs.framework.registerRoute(uptimeRouteWrapper(createRouteWithAuth(libs, route)))
libs.framework.registerRoute(uptimeRouteWrapper(createRouteWithAuth(libs, route), server))
);
const {