-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Changes from 1 commit
cb994e5
f1228ea
088de64
da01ddd
76542c0
cc680b3
00891b7
d820dc8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||
}); | ||
|
||
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>; |
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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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']), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
}); | ||
} | ||
}; | ||
|
||
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; | ||
|
@@ -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, | ||
|
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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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-*'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be indexing to both There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
privileges: ['view_index_metadata', 'create_doc'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We will need There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' | ||
); | ||
}); | ||
}); |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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