mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[APM] Add Agent Keys in APM settings - Create agent keys (#120373)
* Add support for creating agent keys Co-authored-by: Casper Hübertz <casper@formgeist.com> Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
This commit is contained in:
parent
760374f5a0
commit
79e013f9b1
9 changed files with 655 additions and 58 deletions
13
x-pack/plugins/apm/common/agent_key_types.ts
Normal file
13
x-pack/plugins/apm/common/agent_key_types.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 interface CreateApiKeyResponse {
|
||||
api_key: string;
|
||||
expiration?: number;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
|
@ -18,10 +18,10 @@ import { ConfirmDeleteModal } from './confirm_delete_modal';
|
|||
|
||||
interface Props {
|
||||
agentKeys: ApiKey[];
|
||||
refetchAgentKeys: () => void;
|
||||
onKeyDelete: () => void;
|
||||
}
|
||||
|
||||
export function AgentKeysTable({ agentKeys, refetchAgentKeys }: Props) {
|
||||
export function AgentKeysTable({ agentKeys, onKeyDelete }: Props) {
|
||||
const [agentKeyToBeDeleted, setAgentKeyToBeDeleted] = useState<ApiKey>();
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<ApiKey>> = [
|
||||
|
@ -159,7 +159,7 @@ export function AgentKeysTable({ agentKeys, refetchAgentKeys }: Props) {
|
|||
agentKey={agentKeyToBeDeleted}
|
||||
onConfirm={() => {
|
||||
setAgentKeyToBeDeleted(undefined);
|
||||
refetchAgentKeys();
|
||||
onKeyDelete();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
* 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 React, { useState, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
EuiFlyoutBody,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyoutFooter,
|
||||
EuiButtonEmpty,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiFieldText,
|
||||
EuiText,
|
||||
EuiFormFieldset,
|
||||
EuiCheckbox,
|
||||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { callApmApi } from '../../../../services/rest/createCallApmApi';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ApmPluginStartDeps } from '../../../../plugin';
|
||||
import { CreateApiKeyResponse } from '../../../../../common/agent_key_types';
|
||||
|
||||
interface Props {
|
||||
onCancel: () => void;
|
||||
onSuccess: (agentKey: CreateApiKeyResponse) => void;
|
||||
onError: (keyName: string) => void;
|
||||
}
|
||||
|
||||
export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {
|
||||
const {
|
||||
services: { security },
|
||||
} = useKibana<ApmPluginStartDeps>();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
|
||||
const [formTouched, setFormTouched] = useState(false);
|
||||
const [keyName, setKeyName] = useState('');
|
||||
const [agentConfigChecked, setAgentConfigChecked] = useState(true);
|
||||
const [eventWriteChecked, setEventWriteChecked] = useState(true);
|
||||
const [sourcemapChecked, setSourcemapChecked] = useState(true);
|
||||
|
||||
const isInputInvalid = isEmpty(keyName);
|
||||
const isFormInvalid = formTouched && isInputInvalid;
|
||||
|
||||
const formError = i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.name.placeholder',
|
||||
{ defaultMessage: 'Enter a name' }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const getCurrentUser = async () => {
|
||||
try {
|
||||
const authenticatedUser = await security?.authc.getCurrentUser();
|
||||
setUsername(authenticatedUser?.username || '');
|
||||
} catch {
|
||||
setUsername('');
|
||||
}
|
||||
};
|
||||
getCurrentUser();
|
||||
}, [security?.authc]);
|
||||
|
||||
const createAgentKeyTitle = i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.createAgentKey',
|
||||
{ defaultMessage: 'Create agent key' }
|
||||
);
|
||||
|
||||
const createAgentKey = async () => {
|
||||
setFormTouched(true);
|
||||
if (isInputInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { agentKey } = await callApmApi({
|
||||
endpoint: 'POST /apm/agent_keys',
|
||||
signal: null,
|
||||
params: {
|
||||
body: {
|
||||
name: keyName,
|
||||
sourcemap: sourcemapChecked,
|
||||
event: eventWriteChecked,
|
||||
agentConfig: agentConfigChecked,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
onSuccess(agentKey);
|
||||
} catch (error) {
|
||||
onError(keyName);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onCancel} size="s">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle>
|
||||
<h2>{createAgentKeyTitle}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody>
|
||||
<EuiForm isInvalid={isFormInvalid} error={formError}>
|
||||
{username && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.userTitle',
|
||||
{ defaultMessage: 'User' }
|
||||
)}
|
||||
>
|
||||
<EuiText>{username}</EuiText>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.nameTitle',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.nameHelpText',
|
||||
{
|
||||
defaultMessage: 'What is this key used for?',
|
||||
}
|
||||
)}
|
||||
isInvalid={isFormInvalid}
|
||||
error={formError}
|
||||
>
|
||||
<EuiFieldText
|
||||
name="name"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.namePlaceholder',
|
||||
{
|
||||
defaultMessage: 'e.g. apm-key',
|
||||
}
|
||||
)}
|
||||
onChange={(e) => setKeyName(e.target.value)}
|
||||
isInvalid={isFormInvalid}
|
||||
onBlur={() => setFormTouched(true)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormFieldset
|
||||
legend={{
|
||||
children: i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.privilegesFieldset',
|
||||
{
|
||||
defaultMessage: 'Assign privileges',
|
||||
}
|
||||
),
|
||||
}}
|
||||
>
|
||||
<EuiFormRow
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.agentConfigHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Required for agents to read agent configuration remotely.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiCheckbox
|
||||
id={htmlIdGenerator()()}
|
||||
label="config_agent:read"
|
||||
checked={agentConfigChecked}
|
||||
onChange={() => setAgentConfigChecked((state) => !state)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.ingestAgentEvents',
|
||||
{
|
||||
defaultMessage: 'Required for ingesting events.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiCheckbox
|
||||
id={htmlIdGenerator()()}
|
||||
label="event:write"
|
||||
checked={eventWriteChecked}
|
||||
onChange={() => setEventWriteChecked((state) => !state)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.sourcemaps',
|
||||
{
|
||||
defaultMessage: 'Required for uploading sourcemaps.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiCheckbox
|
||||
id={htmlIdGenerator()()}
|
||||
label="sourcemap:write"
|
||||
checked={sourcemapChecked}
|
||||
onChange={() => setSourcemapChecked((state) => !state)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFormFieldset>
|
||||
</EuiForm>
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onCancel}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createKeyFlyout.cancelButton',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill={true}
|
||||
onClick={createAgentKey}
|
||||
type="submit"
|
||||
disabled={isFormInvalid}
|
||||
>
|
||||
{createAgentKeyTitle}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
|
@ -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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiCallOut,
|
||||
EuiButtonIcon,
|
||||
EuiCopy,
|
||||
EuiFormControlLayout,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export function AgentKeyCallOut({ name, token }: Props) {
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.copyAgentKeyField.title',
|
||||
{
|
||||
defaultMessage: 'Created "{name}" key',
|
||||
values: { name },
|
||||
}
|
||||
)}
|
||||
color="success"
|
||||
iconType="check"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.copyAgentKeyField.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'Copy this key now. You will not be able to view it again.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<EuiFormControlLayout
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
readOnly
|
||||
prepend="Base64"
|
||||
append={
|
||||
<EuiCopy textToCopy={token}>
|
||||
{(copy) => (
|
||||
<EuiButtonIcon
|
||||
iconType="copyClipboard"
|
||||
onClick={copy}
|
||||
color="success"
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.copyAgentKeyField.copyButton',
|
||||
{
|
||||
defaultMessage: 'Copy to clipboard',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</EuiCopy>
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
className="euiFieldText euiFieldText--inGroup"
|
||||
readOnly
|
||||
value={token}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.copyAgentKeyField.agentKeyLabel',
|
||||
{
|
||||
defaultMessage: 'Agent key',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFormControlLayout>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
|
@ -21,6 +21,11 @@ import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
|||
import { PermissionDenied } from './prompts/permission_denied';
|
||||
import { ApiKeysNotEnabled } from './prompts/api_keys_not_enabled';
|
||||
import { AgentKeysTable } from './agent_keys_table';
|
||||
import { CreateAgentKeyFlyout } from './create_agent_key';
|
||||
import { AgentKeyCallOut } from './create_agent_key/agent_key_callout';
|
||||
import { CreateApiKeyResponse } from '../../../../../common/agent_key_types';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { ApiKey } from '../../../../../../security/common/model';
|
||||
|
||||
const INITIAL_DATA = {
|
||||
areApiKeysEnabled: false,
|
||||
|
@ -28,33 +33,12 @@ const INITIAL_DATA = {
|
|||
};
|
||||
|
||||
export function AgentKeys() {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiText color="subdued">
|
||||
{i18n.translate('xpack.apm.settings.agentKeys.descriptionText', {
|
||||
defaultMessage:
|
||||
'View and delete agent keys. An agent key sends requests on behalf of a user.',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.settings.agentKeys.title', {
|
||||
defaultMessage: 'Agent keys',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<AgentKeysContent />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
const { toasts } = useApmPluginContext().core.notifications;
|
||||
|
||||
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
|
||||
const [createdAgentKey, setCreatedAgentKey] =
|
||||
useState<CreateApiKeyResponse>();
|
||||
|
||||
function AgentKeysContent() {
|
||||
const {
|
||||
data: { areApiKeysEnabled, canManage } = INITIAL_DATA,
|
||||
status: privilegesStatus,
|
||||
|
@ -85,16 +69,112 @@ function AgentKeysContent() {
|
|||
);
|
||||
|
||||
const agentKeys = data?.agentKeys;
|
||||
const isLoading =
|
||||
privilegesStatus === FETCH_STATUS.LOADING ||
|
||||
status === FETCH_STATUS.LOADING;
|
||||
|
||||
const requestFailed =
|
||||
privilegesStatus === FETCH_STATUS.FAILURE ||
|
||||
status === FETCH_STATUS.FAILURE;
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiText color="subdued">
|
||||
{i18n.translate('xpack.apm.settings.agentKeys.descriptionText', {
|
||||
defaultMessage:
|
||||
'View and delete agent keys. An agent key sends requests on behalf of a user.',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.settings.agentKeys.title', {
|
||||
defaultMessage: 'Agent keys',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{areApiKeysEnabled && canManage && !isEmpty(agentKeys) && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() => setIsFlyoutVisible(true)}
|
||||
fill={true}
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createAgentKeyButton',
|
||||
{
|
||||
defaultMessage: 'Create agent key',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
{createdAgentKey && (
|
||||
<AgentKeyCallOut
|
||||
name={createdAgentKey.name}
|
||||
token={btoa(`${createdAgentKey.id}:${createdAgentKey.api_key}`)}
|
||||
/>
|
||||
)}
|
||||
{isFlyoutVisible && (
|
||||
<CreateAgentKeyFlyout
|
||||
onCancel={() => {
|
||||
setIsFlyoutVisible(false);
|
||||
}}
|
||||
onSuccess={(agentKey: CreateApiKeyResponse) => {
|
||||
setCreatedAgentKey(agentKey);
|
||||
setIsFlyoutVisible(false);
|
||||
refetchAgentKeys();
|
||||
}}
|
||||
onError={(keyName: string) => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.apm.settings.agentKeys.crate.failed', {
|
||||
defaultMessage: 'Error creating agent key "{keyName}"',
|
||||
values: { keyName },
|
||||
})
|
||||
);
|
||||
setIsFlyoutVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<AgentKeysContent
|
||||
loading={
|
||||
privilegesStatus === FETCH_STATUS.LOADING ||
|
||||
status === FETCH_STATUS.LOADING
|
||||
}
|
||||
requestFailed={
|
||||
privilegesStatus === FETCH_STATUS.FAILURE ||
|
||||
status === FETCH_STATUS.FAILURE
|
||||
}
|
||||
canManage={canManage}
|
||||
areApiKeysEnabled={areApiKeysEnabled}
|
||||
agentKeys={agentKeys}
|
||||
onKeyDelete={() => {
|
||||
setCreatedAgentKey(undefined);
|
||||
refetchAgentKeys();
|
||||
}}
|
||||
onCreateAgentClick={() => setIsFlyoutVisible(true)}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentKeysContent({
|
||||
loading,
|
||||
requestFailed,
|
||||
canManage,
|
||||
areApiKeysEnabled,
|
||||
agentKeys,
|
||||
onKeyDelete,
|
||||
onCreateAgentClick,
|
||||
}: {
|
||||
loading: boolean;
|
||||
requestFailed: boolean;
|
||||
canManage: boolean;
|
||||
areApiKeysEnabled: boolean;
|
||||
agentKeys?: ApiKey[];
|
||||
onKeyDelete: () => void;
|
||||
onCreateAgentClick: () => void;
|
||||
}) {
|
||||
if (!agentKeys) {
|
||||
if (isLoading) {
|
||||
if (loading) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={<EuiLoadingSpinner size="xl" />}
|
||||
|
@ -147,7 +227,7 @@ function AgentKeysContent() {
|
|||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.settings.agentKeys.emptyPromptTitle', {
|
||||
defaultMessage: 'Create your first agent key',
|
||||
defaultMessage: 'Create your first key',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
|
@ -155,12 +235,16 @@ function AgentKeysContent() {
|
|||
<p>
|
||||
{i18n.translate('xpack.apm.settings.agentKeys.emptyPromptBody', {
|
||||
defaultMessage:
|
||||
'Create agent keys to authorize requests to the APM Server.',
|
||||
'Create keys to authorize agent requests to the APM Server.',
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton fill={true} iconType="plusInCircleFilled">
|
||||
<EuiButton
|
||||
onClick={onCreateAgentClick}
|
||||
fill={true}
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentKeys.createAgentKeyButton',
|
||||
{
|
||||
|
@ -175,10 +259,7 @@ function AgentKeysContent() {
|
|||
|
||||
if (agentKeys && !isEmpty(agentKeys)) {
|
||||
return (
|
||||
<AgentKeysTable
|
||||
agentKeys={agentKeys ?? []}
|
||||
refetchAgentKeys={refetchAgentKeys}
|
||||
/>
|
||||
<AgentKeysTable agentKeys={agentKeys ?? []} onKeyDelete={onKeyDelete} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,11 +18,11 @@ import { getLegacyApmHref } from '../../shared/Links/apm/APMLink';
|
|||
type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
|
||||
key:
|
||||
| 'agent-configurations'
|
||||
| 'agent-keys'
|
||||
| 'anomaly-detection'
|
||||
| 'apm-indices'
|
||||
| 'customize-ui'
|
||||
| 'schema'
|
||||
| 'agent-keys';
|
||||
| 'schema';
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
|
@ -76,6 +76,17 @@ function getTabs({
|
|||
search,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'agent-keys',
|
||||
label: i18n.translate('xpack.apm.settings.agentKeys', {
|
||||
defaultMessage: 'Agent Keys',
|
||||
}),
|
||||
href: getLegacyApmHref({
|
||||
basePath,
|
||||
path: `/settings/agent-keys`,
|
||||
search,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'anomaly-detection',
|
||||
label: i18n.translate('xpack.apm.settings.anomalyDetection', {
|
||||
|
@ -117,17 +128,6 @@ function getTabs({
|
|||
}),
|
||||
href: getLegacyApmHref({ basePath, path: `/settings/schema`, search }),
|
||||
},
|
||||
{
|
||||
key: 'agent-keys',
|
||||
label: i18n.translate('xpack.apm.settings.agentKeys', {
|
||||
defaultMessage: 'Agent Keys',
|
||||
}),
|
||||
href: getLegacyApmHref({
|
||||
basePath,
|
||||
path: `/settings/agent-keys`,
|
||||
search,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return tabs
|
||||
|
|
|
@ -54,6 +54,8 @@ import { getLazyApmAgentsTabExtension } from './components/fleet_integration/laz
|
|||
import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/lazy_apm_policy_create_extension';
|
||||
import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension';
|
||||
import { featureCatalogueEntry } from './featureCatalogueEntry';
|
||||
import type { SecurityPluginStart } from '../../security/public';
|
||||
|
||||
export type ApmPluginSetup = ReturnType<ApmPlugin['setup']>;
|
||||
|
||||
export type ApmPluginStart = void;
|
||||
|
@ -81,6 +83,7 @@ export interface ApmPluginStartDeps {
|
|||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
observability: ObservabilityPublicStart;
|
||||
fleet?: FleetStart;
|
||||
security?: SecurityPluginStart;
|
||||
}
|
||||
|
||||
const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', {
|
||||
|
|
138
x-pack/plugins/apm/server/routes/agent_keys/create_agent_key.ts
Normal file
138
x-pack/plugins/apm/server/routes/agent_keys/create_agent_key.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { ApmPluginRequestHandlerContext } from '../typings';
|
||||
import { CreateApiKeyResponse } from '../../../common/agent_key_types';
|
||||
|
||||
const enum PrivilegeType {
|
||||
SOURCEMAP = 'sourcemap:write',
|
||||
EVENT = 'event:write',
|
||||
AGENT_CONFIG = 'config_agent:read',
|
||||
}
|
||||
|
||||
interface SecurityHasPrivilegesResponse {
|
||||
application: {
|
||||
apm: {
|
||||
'-': {
|
||||
[PrivilegeType.SOURCEMAP]: boolean;
|
||||
[PrivilegeType.EVENT]: boolean;
|
||||
[PrivilegeType.AGENT_CONFIG]: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
has_all_requested: boolean;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export async function createAgentKey({
|
||||
context,
|
||||
requestBody,
|
||||
}: {
|
||||
context: ApmPluginRequestHandlerContext;
|
||||
requestBody: {
|
||||
name: string;
|
||||
sourcemap?: boolean;
|
||||
event?: boolean;
|
||||
agentConfig?: boolean;
|
||||
};
|
||||
}) {
|
||||
// Elasticsearch will allow a user without the right apm privileges to create API keys, but the keys won't validate
|
||||
// check first whether the user has the right privileges, and bail out early if not
|
||||
const {
|
||||
body: { application, username, has_all_requested: hasRequiredPrivileges },
|
||||
} = await context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges<SecurityHasPrivilegesResponse>(
|
||||
{
|
||||
body: {
|
||||
application: [
|
||||
{
|
||||
application: 'apm',
|
||||
privileges: [
|
||||
PrivilegeType.SOURCEMAP,
|
||||
PrivilegeType.EVENT,
|
||||
PrivilegeType.AGENT_CONFIG,
|
||||
],
|
||||
resources: ['-'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!hasRequiredPrivileges) {
|
||||
const missingPrivileges = Object.entries(application.apm['-'])
|
||||
.filter((x) => !x[1])
|
||||
.map((x) => x[0])
|
||||
.join(', ');
|
||||
const error = `${username} is missing the following requested privilege(s): ${missingPrivileges}.\
|
||||
You might try with the superuser, or add the APM application privileges to the role of the authenticated user, eg.:
|
||||
PUT /_security/role/my_role {
|
||||
...
|
||||
"applications": [{
|
||||
"application": "apm",
|
||||
"privileges": ["sourcemap:write", "event:write", "config_agent:read"],
|
||||
"resources": ["*"]
|
||||
}],
|
||||
...
|
||||
}`;
|
||||
throw Boom.internal(error);
|
||||
}
|
||||
|
||||
const { name = 'apm-key', sourcemap, event, agentConfig } = requestBody;
|
||||
|
||||
const privileges: PrivilegeType[] = [];
|
||||
if (!sourcemap && !event && !agentConfig) {
|
||||
privileges.push(
|
||||
PrivilegeType.SOURCEMAP,
|
||||
PrivilegeType.EVENT,
|
||||
PrivilegeType.AGENT_CONFIG
|
||||
);
|
||||
}
|
||||
|
||||
if (sourcemap) {
|
||||
privileges.push(PrivilegeType.SOURCEMAP);
|
||||
}
|
||||
|
||||
if (event) {
|
||||
privileges.push(PrivilegeType.EVENT);
|
||||
}
|
||||
|
||||
if (agentConfig) {
|
||||
privileges.push(PrivilegeType.AGENT_CONFIG);
|
||||
}
|
||||
|
||||
const body = {
|
||||
name,
|
||||
metadata: {
|
||||
application: 'apm',
|
||||
},
|
||||
role_descriptors: {
|
||||
apm: {
|
||||
cluster: [],
|
||||
index: [],
|
||||
applications: [
|
||||
{
|
||||
application: 'apm',
|
||||
privileges,
|
||||
resources: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { body: agentKey } =
|
||||
await context.core.elasticsearch.client.asCurrentUser.security.createApiKey<CreateApiKeyResponse>(
|
||||
{
|
||||
body,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
agentKey,
|
||||
};
|
||||
}
|
|
@ -8,11 +8,13 @@
|
|||
import Boom from '@hapi/boom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import * as t from 'io-ts';
|
||||
import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository';
|
||||
import { getAgentKeys } from './get_agent_keys';
|
||||
import { getAgentKeysPrivileges } from './get_agent_keys_privileges';
|
||||
import { invalidateAgentKey } from './invalidate_agent_key';
|
||||
import { createAgentKey } from './create_agent_key';
|
||||
|
||||
const agentKeysRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/agent_keys',
|
||||
|
@ -74,10 +76,40 @@ const invalidateAgentKeyRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const createAgentKeyRoute = createApmServerRoute({
|
||||
endpoint: 'POST /apm/agent_keys',
|
||||
options: { tags: ['access:apm', 'access:apm_write'] },
|
||||
params: t.type({
|
||||
body: t.intersection([
|
||||
t.partial({
|
||||
sourcemap: toBooleanRt,
|
||||
event: toBooleanRt,
|
||||
agentConfig: toBooleanRt,
|
||||
}),
|
||||
t.type({
|
||||
name: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
handler: async (resources) => {
|
||||
const { context, params } = resources;
|
||||
|
||||
const { body: requestBody } = params;
|
||||
|
||||
const agentKey = await createAgentKey({
|
||||
context,
|
||||
requestBody,
|
||||
});
|
||||
|
||||
return agentKey;
|
||||
},
|
||||
});
|
||||
|
||||
export const agentKeysRouteRepository = createApmServerRouteRepository()
|
||||
.add(agentKeysRoute)
|
||||
.add(agentKeysPrivilegesRoute)
|
||||
.add(invalidateAgentKeyRoute);
|
||||
.add(invalidateAgentKeyRoute)
|
||||
.add(createAgentKeyRoute);
|
||||
|
||||
const SECURITY_REQUIRED_MESSAGE = i18n.translate(
|
||||
'xpack.apm.api.apiKeys.securityRequired',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue