Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Uptime] Generate api key for synthetics service #119590

Merged
merged 8 commits into from
Nov 25, 2021
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, I've also seen the api service return an encoded key on the object, which is the encoded id and name which saves us the trouble of having to encode it ourselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we are not using the encoded part, since we only need apiKey and that will get saved in savedObjects in encrypted form. But will add it just in case for typing purpose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are not saving encoded part at all in the saved object

});

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>;
8 changes: 5 additions & 3 deletions x-pack/plugins/uptime/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
"requiredPlugins": [
"alerting",
"embeddable",
"encryptedSavedObjects",
"inspector",
"features",
"licensing",
"triggersActionsUi",
"usageCollection",
"observability",
"ruleRegistry",
"observability"
"security",
"triggersActionsUi",
"usageCollection"
],
"server": true,
"ui": true,
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/uptime/server/kibana.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -29,7 +29,7 @@ export interface KibanaServer extends Server {

export const initServerWithKibana = (
server: UptimeCoreSetup,
plugins: UptimeCorePlugins,
plugins: UptimeCorePluginsSetup,
ruleRegistry: UptimeRuleRegistry,
logger: Logger
) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ import type {
IScopedClusterClient,
} from 'src/core/server';
import { ObservabilityPluginSetup } from '../../../../../observability/server';
import { PluginSetupContract as AlertingPLuginSetupContract } from '../../../../../alerting/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 { UptimeConfig } from '../../../config';
import { SecurityPluginStart } from '../../../../../security/server';

export type UMElasticsearchQueryFn<P, R = any> = (
params: {
Expand All @@ -35,16 +41,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;
alerting: AlertingPLuginSetupContract;
observability: ObservabilityPluginSetup;
usageCollection: UsageCollectionSetup;
ml: MlSetup;
ruleRegistry: RuleRegistryPluginSetupContract;
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
}

export interface UptimeCorePluginsStart {
security: SecurityPluginStart;
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
}

export interface UMBackendFrameworkAdapter {
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 };
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/uptime/server/lib/alerts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>;
8 changes: 8 additions & 0 deletions x-pack/plugins/uptime/server/lib/saved_objects/index.ts
Original file line number Diff line number Diff line change
@@ -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';
26 changes: 18 additions & 8 deletions x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,43 @@ 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 '../../config';
import { settingsObjectId, umDynamicSettings } from './uptime_settings';
import { syntheticsMonitor } from './synthetics_monitor';

export interface UMSavedObjectsAdapter {
config: UptimeConfig;
getUptimeDynamicSettings: UMSavedObjectsQueryFn<DynamicSettings>;
setUptimeDynamicSettings: UMSavedObjectsQueryFn<void, DynamicSettings>;
}
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']),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned above, when I was testing this the saved object service also returned to me an encoded key, which was the pre-encoded id:apiKey combination. If that's the case, we should also encrypted that key, or only save the encoded key on the saved object and encrypted that.

});
}
};

export interface UMSavedObjectsAdapter {
config: UptimeConfig;
getUptimeDynamicSettings: UMSavedObjectsQueryFn<DynamicSettings>;
setUptimeDynamicSettings: UMSavedObjectsQueryFn<void, DynamicSettings>;
}

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;
Expand All @@ -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,
Expand Down
74 changes: 74 additions & 0 deletions x-pack/plugins/uptime/server/lib/saved_objects/service_api_key.ts
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using UUID is a requirement to work with encrypted saved objects

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, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need an error handling? Unsure of what potential points of failure there are.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think so, it will throw an error, kibana route will auto pick it up and generate a message if it needs be.

id: syntheticsApiKeyID,
overwrite: true,
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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-*', 'heartbeat-*'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be indexing to both heartbeat-* and synthetics-*? I was under the impression we should only be indexing to synthetics-*

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm realizing that I'm commenting this on the test, but it applies to the actual implementation too. Same for the below comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we are going to use data stream format, it makes sense to remove heartbeat, i will do that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

privileges: ['view_index_metadata', 'create_doc'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need auto_configure privilege as well to create the data stream

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

},
],
},
},
}
);
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'
);
});
});
Loading