Skip to content

Commit

Permalink
[APM] agent keys management improvements (elastic#120765)
Browse files Browse the repository at this point in the history
* Add userCurrentUser hook

* Use EuiFieldText instead of input element

* Display error messages in the UI when creating agent keys

* Remove default agent key name

* Prefix createAgentKeyRoute with /api

* Fix issue where you cannot invalidate API keys when you only have manage_own_api_key privilege

Co-authored-by: Casper Hübertz <casper@formgeist.com>
  • Loading branch information
2 people authored and kibanamachine committed Dec 9, 2021
1 parent d9aed2a commit 87cfd9d
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 145 deletions.
22 changes: 22 additions & 0 deletions x-pack/plugins/apm/common/privilege_type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 enum PrivilegeType {
SOURCEMAP = 'sourcemap:write',
EVENT = 'event:write',
AGENT_CONFIG = 'config_agent:read',
}

export const privilegesTypeRt = t.array(
t.union([
t.literal(PrivilegeType.SOURCEMAP),
t.literal(PrivilegeType.EVENT),
t.literal(PrivilegeType.AGENT_CONFIG),
])
);
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function AgentKeysTable({ agentKeys, onKeyDelete }: Props) {
description: i18n.translate(
'xpack.apm.settings.agentKeys.table.deleteActionDescription',
{
defaultMessage: 'Delete this agent key',
defaultMessage: 'Delete this APM agent key',
}
),
icon: 'trash',
Expand Down Expand Up @@ -144,7 +144,7 @@ export function AgentKeysTable({ agentKeys, onKeyDelete }: Props) {
tableCaption={i18n.translate(
'xpack.apm.settings.agentKeys.tableCaption',
{
defaultMessage: 'Agent keys',
defaultMessage: 'APM agent keys',
}
)}
items={agentKeys ?? []}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ export function ConfirmDeleteModal({ agentKey, onCancel, onConfirm }: Props) {
});
toasts.addSuccess(
i18n.translate('xpack.apm.settings.agentKeys.invalidate.succeeded', {
defaultMessage: 'Deleted agent key "{name}"',
defaultMessage: 'Deleted APM agent key "{name}"',
values: { name },
})
);
} catch (error) {
toasts.addDanger(
i18n.translate('xpack.apm.settings.agentKeys.invalidate.failed', {
defaultMessage: 'Error deleting agent key "{name}"',
defaultMessage: 'Error deleting APM agent key "{name}"',
values: { name },
})
);
Expand All @@ -53,7 +53,7 @@ export function ConfirmDeleteModal({ agentKey, onCancel, onConfirm }: Props) {
title={i18n.translate(
'xpack.apm.settings.agentKeys.deleteConfirmModal.title',
{
defaultMessage: 'Delete agent key "{name}"?',
defaultMessage: 'Delete APM agent key "{name}"?',
values: { name },
}
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButton,
Expand All @@ -28,52 +28,41 @@ import {
} 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';
import { useCurrentUser } from '../../../../hooks/use_current_user';
import { PrivilegeType } from '../../../../../common/privilege_type';

interface Props {
onCancel: () => void;
onSuccess: (agentKey: CreateApiKeyResponse) => void;
onError: (keyName: string) => void;
onError: (keyName: string, message: string) => void;
}

export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {
const {
services: { security },
} = useKibana<ApmPluginStartDeps>();
const [formTouched, setFormTouched] = useState(false);

const [username, setUsername] = useState('');
const [agentKeyBody, setAgentKeyBody] = useState({
name: '',
sourcemap: true,
event: true,
agentConfig: true,
});

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 { name, sourcemap, event, agentConfig } = agentKeyBody;

const isInputInvalid = isEmpty(keyName);
const currentUser = useCurrentUser();

const isInputInvalid = isEmpty(name);
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' }
{ defaultMessage: 'Create APM agent key' }
);

const createAgentKey = async () => {
Expand All @@ -83,22 +72,33 @@ export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {
}

try {
const privileges: PrivilegeType[] = [];
if (sourcemap) {
privileges.push(PrivilegeType.SOURCEMAP);
}

if (event) {
privileges.push(PrivilegeType.EVENT);
}

if (agentConfig) {
privileges.push(PrivilegeType.AGENT_CONFIG);
}

const { agentKey } = await callApmApi({
endpoint: 'POST /apm/agent_keys',
endpoint: 'POST /api/apm/agent_keys',
signal: null,
params: {
body: {
name: keyName,
sourcemap: sourcemapChecked,
event: eventWriteChecked,
agentConfig: agentConfigChecked,
name,
privileges,
},
},
});

onSuccess(agentKey);
} catch (error) {
onError(keyName);
onError(name, error.body?.message || error.message);
}
};

Expand All @@ -112,14 +112,14 @@ export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {

<EuiFlyoutBody>
<EuiForm isInvalid={isFormInvalid} error={formError}>
{username && (
{currentUser && (
<EuiFormRow
label={i18n.translate(
'xpack.apm.settings.agentKeys.createKeyFlyout.userTitle',
{ defaultMessage: 'User' }
)}
>
<EuiText>{username}</EuiText>
<EuiText>{currentUser?.username}</EuiText>
</EuiFormRow>
)}
<EuiFormRow
Expand All @@ -146,7 +146,9 @@ export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {
defaultMessage: 'e.g. apm-key',
}
)}
onChange={(e) => setKeyName(e.target.value)}
onChange={(e) =>
setAgentKeyBody((state) => ({ ...state, name: e.target.value }))
}
isInvalid={isFormInvalid}
onBlur={() => setFormTouched(true)}
/>
Expand Down Expand Up @@ -174,8 +176,13 @@ export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {
<EuiCheckbox
id={htmlIdGenerator()()}
label="config_agent:read"
checked={agentConfigChecked}
onChange={() => setAgentConfigChecked((state) => !state)}
checked={agentConfig}
onChange={() =>
setAgentKeyBody((state) => ({
...state,
agentConfig: !state.agentConfig,
}))
}
/>
</EuiFormRow>
<EuiSpacer size="s" />
Expand All @@ -190,8 +197,13 @@ export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {
<EuiCheckbox
id={htmlIdGenerator()()}
label="event:write"
checked={eventWriteChecked}
onChange={() => setEventWriteChecked((state) => !state)}
checked={event}
onChange={() =>
setAgentKeyBody((state) => ({
...state,
event: !state.event,
}))
}
/>
</EuiFormRow>
<EuiSpacer size="s" />
Expand All @@ -206,8 +218,13 @@ export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) {
<EuiCheckbox
id={htmlIdGenerator()()}
label="sourcemap:write"
checked={sourcemapChecked}
onChange={() => setSourcemapChecked((state) => !state)}
checked={sourcemap}
onChange={() =>
setAgentKeyBody((state) => ({
...state,
sourcemap: !state.sourcemap,
}))
}
/>
</EuiFormRow>
<EuiSpacer size="s" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
EuiCallOut,
EuiButtonIcon,
EuiCopy,
EuiFormControlLayout,
EuiFieldText,
} from '@elastic/eui';

interface Props {
Expand Down Expand Up @@ -43,9 +43,15 @@ export function AgentKeyCallOut({ name, token }: Props) {
}
)}
</p>
<EuiFormControlLayout
style={{ backgroundColor: 'transparent' }}
<EuiFieldText
readOnly
value={token}
aria-label={i18n.translate(
'xpack.apm.settings.agentKeys.copyAgentKeyField.agentKeyLabel',
{
defaultMessage: 'APM agent key',
}
)}
prepend="Base64"
append={
<EuiCopy textToCopy={token}>
Expand All @@ -65,20 +71,7 @@ export function AgentKeyCallOut({ name, token }: Props) {
)}
</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" />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function AgentKeys() {
<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.',
'View and delete APM agent keys. An APM agent key sends requests on behalf of a user.',
})}
</EuiText>
<EuiSpacer size="m" />
Expand All @@ -84,7 +84,7 @@ export function AgentKeys() {
<EuiTitle>
<h2>
{i18n.translate('xpack.apm.settings.agentKeys.title', {
defaultMessage: 'Agent keys',
defaultMessage: 'APM agent keys',
})}
</h2>
</EuiTitle>
Expand All @@ -99,7 +99,7 @@ export function AgentKeys() {
{i18n.translate(
'xpack.apm.settings.agentKeys.createAgentKeyButton',
{
defaultMessage: 'Create agent key',
defaultMessage: 'Create APM agent key',
}
)}
</EuiButton>
Expand All @@ -123,11 +123,12 @@ export function AgentKeys() {
setIsFlyoutVisible(false);
refetchAgentKeys();
}}
onError={(keyName: string) => {
onError={(keyName: string, message: string) => {
toasts.addDanger(
i18n.translate('xpack.apm.settings.agentKeys.crate.failed', {
defaultMessage: 'Error creating agent key "{keyName}"',
values: { keyName },
defaultMessage:
'Error creating APM agent key "{keyName}". Error: "{message}"',
values: { keyName, message },
})
);
setIsFlyoutVisible(false);
Expand Down Expand Up @@ -184,7 +185,7 @@ function AgentKeysContent({
{i18n.translate(
'xpack.apm.settings.agentKeys.agentKeysLoadingPromptTitle',
{
defaultMessage: 'Loading Agent keys...',
defaultMessage: 'Loading APM agent keys...',
}
)}
</h2>
Expand All @@ -202,7 +203,7 @@ function AgentKeysContent({
{i18n.translate(
'xpack.apm.settings.agentKeys.agentKeysErrorPromptTitle',
{
defaultMessage: 'Could not load agent keys.',
defaultMessage: 'Could not load APM agent keys.',
}
)}
</h2>
Expand Down Expand Up @@ -235,7 +236,7 @@ function AgentKeysContent({
<p>
{i18n.translate('xpack.apm.settings.agentKeys.emptyPromptBody', {
defaultMessage:
'Create keys to authorize agent requests to the APM Server.',
'Create APM agent keys to authorize APM agent requests to the APM Server.',
})}
</p>
}
Expand All @@ -248,7 +249,7 @@ function AgentKeysContent({
{i18n.translate(
'xpack.apm.settings.agentKeys.createAgentKeyButton',
{
defaultMessage: 'Create agent key',
defaultMessage: 'Create APM agent key',
}
)}
</EuiButton>
Expand Down
Loading

0 comments on commit 87cfd9d

Please sign in to comment.