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

[Logs Onboarding] API key generation states #159207

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* 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 {
EuiButtonIcon,
EuiCallOut,
EuiCopy,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
} from '@elastic/eui';
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
import { i18n } from '@kbn/i18n';
import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
import React from 'react';
import { APIReturnType } from '../../../../services/rest/create_call_api';

type ApiKeyPayload =
APIReturnType<'POST /internal/observability_onboarding/custom_logs/save'>;

export function ApiKeyBanner({
status,
payload,
error,
}: {
status: FETCH_STATUS | 'noPrivileges';
yngrdyn marked this conversation as resolved.
Show resolved Hide resolved
payload?: ApiKeyPayload;
error?: IHttpFetchError<ResponseErrorBody>;
}) {
const loadingCallout = (
<EuiCallOut
title={
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
<EuiFlexItem>
{i18n.translate(
'xpack.observability_onboarding.apiKeyBanner.loading',
{
defaultMessage: 'Creating API Key',
}
)}
</EuiFlexItem>
</EuiFlexGroup>
}
color="primary"
/>
);

const apiKeySuccessCallout = (
<EuiCallOut
title={i18n.translate(
'xpack.observability_onboarding.apiKeyBanner.created',
{
defaultMessage: 'API Key created.',
}
)}
color="success"
iconType="check"
>
<p>
{i18n.translate(
'xpack.observability_onboarding.apiKeyBanner.created.description',
{
defaultMessage:
'Remember to store this information in a safe place. It won’t be displayed anymore after you continue.',
}
)}
</p>
<EuiFieldText
data-test-subj="apmAgentKeyCallOutFieldText"
readOnly
value={payload?.apiKeyEncoded}
aria-label={i18n.translate(
'xpack.observability_onboarding.apiKeyBanner.field.label',
{
defaultMessage: 'Api Key',
}
)}
append={
<EuiCopy textToCopy={payload?.apiKeyEncoded ?? ''}>
{(copy) => (
<EuiButtonIcon
iconType="copyClipboard"
onClick={copy}
color="success"
style={{ backgroundColor: 'transparent' }}
aria-label={i18n.translate(
'xpack.observability_onboarding.apiKeyBanner.field.copyButton',
{
defaultMessage: 'Copy to clipboard',
}
)}
/>
)}
</EuiCopy>
}
/>
</EuiCallOut>
);

const apiKeyFailureCallout = (
<EuiCallOut
title={i18n.translate(
'xpack.observability_onboarding.apiKeyBanner.failed',
{
defaultMessage: 'Failed to create API key.',
}
)}
color="danger"
iconType="error"
>
<p>
{i18n.translate(
'xpack.observability_onboarding.apiKeyBanner.failed.description',
{
defaultMessage: 'Something went wrong: {message}',
values: {
message: error?.body?.message,
},
}
)}
</p>
</EuiCallOut>
);

const noPermissionsCallout = (
<EuiCallOut
title={i18n.translate(
'xpack.observability_onboarding.apiKeyBanner.noPermissions',
{
defaultMessage: 'User does not have permissions to create API key.',
}
)}
color="warning"
iconType="warning"
>
<p>
{i18n.translate(
'xpack.observability_onboarding.apiKeyBanner.noPermissions.description',
{
defaultMessage:
'Please add the key generated by the admin in the dedicated space in the config below.',
}
)}
</p>
</EuiCallOut>
);

if (status === 'noPrivileges') {
return noPermissionsCallout;
}

if (status === FETCH_STATUS.SUCCESS) {
return apiKeySuccessCallout;
}

if (status === FETCH_STATUS.FAILURE) {
return apiKeyFailureCallout;
}

return loadingCallout;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@
* 2.0.
*/

import { Buffer } from 'buffer';
import { flatten, zip } from 'lodash';
import React, { useState } from 'react';
import {
EuiText,
EuiButton,
EuiSpacer,
EuiButtonGroup,
EuiCodeBlock,
EuiSteps,
EuiSkeletonRectangle,
EuiSpacer,
EuiSteps,
EuiText,
} from '@elastic/eui';
import { Buffer } from 'buffer';
import { flatten, zip } from 'lodash';
import React, { useState } from 'react';
import { useWizard } from '.';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import {
StepPanel,
StepPanelContent,
StepPanelFooter,
} from '../../../shared/step_panel';
import { useWizard } from '.';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { ApiKeyBanner } from './api_key_banner';

type ElasticAgentPlatform = 'linux-tar' | 'macos' | 'windows';
export function InstallElasticAgent() {
Expand All @@ -40,11 +41,37 @@ export function InstallElasticAgent() {
goBack();
}

const { data: installShipperSetup, status: installShipperSetupStatus } =
useFetcher((callApi) => {
const { data: monitoringRole, status: monitoringRoleStatus } = useFetcher(
(callApi) => {
if (CurrentStep === InstallElasticAgent) {
return callApi(
'POST /internal/observability_onboarding/custom_logs/install_shipper_setup',
'GET /internal/observability_onboarding/custom_logs/privileges'
);
}
},
[]
);

const { data: setup } = useFetcher((callApi) => {
if (CurrentStep === InstallElasticAgent) {
return callApi(
'GET /internal/observability_onboarding/custom_logs/install_shipper_setup'
);
}
}, []);

const {
data: installShipperSetup,
status: installShipperSetupStatus,
error,
} = useFetcher(
(callApi) => {
if (
CurrentStep === InstallElasticAgent &&
monitoringRole?.hasPrivileges
) {
return callApi(
'POST /internal/observability_onboarding/custom_logs/save',
{
params: {
body: {
Expand All @@ -60,22 +87,26 @@ export function InstallElasticAgent() {
}
);
}
}, []);
},
[monitoringRole?.hasPrivileges]
);

const { data: yamlConfig = '', status: yamlConfigStatus } = useFetcher(
(callApi) => {
if (CurrentStep === InstallElasticAgent && installShipperSetup) {
if (CurrentStep === InstallElasticAgent) {
const options = {
headers: {
authorization: `ApiKey ${installShipperSetup?.apiKeyEncoded}`,
},
};

return callApi(
'GET /api/observability_onboarding/elastic_agent/config 2023-05-24',
{
headers: {
authorization: `ApiKey ${installShipperSetup.apiKeyEncoded}`,
},
}
installShipperSetup?.apiKeyEncoded ? options : {}
);
}
},
[installShipperSetup?.apiKeyId, installShipperSetup?.apiKeyEncoded]
[installShipperSetup?.apiKeyEncoded]
);

const apiKeyEncoded = installShipperSetup?.apiKeyEncoded;
Expand All @@ -93,6 +124,19 @@ export function InstallElasticAgent() {
</p>
</EuiText>
<EuiSpacer size="m" />
{monitoringRoleStatus !== FETCH_STATUS.NOT_INITIATED &&
monitoringRoleStatus !== FETCH_STATUS.LOADING && (
<ApiKeyBanner
payload={installShipperSetup}
status={
monitoringRole?.hasPrivileges
? installShipperSetupStatus
: 'noPrivileges'
}
error={error}
/>
)}
<EuiSpacer size="m" />
<EuiSteps
steps={[
{
Expand Down Expand Up @@ -127,25 +171,14 @@ export function InstallElasticAgent() {
}
/>
<EuiSpacer size="m" />
<EuiSkeletonRectangle
isLoading={
installShipperSetupStatus === FETCH_STATUS.LOADING
}
contentAriaLabel="Command to install elastic agent"
width="100%"
height={80}
borderRadius="s"
>
<EuiCodeBlock language="bash" isCopyable>
{getInstallShipperCommand({
elasticAgentPlatform,
apiKeyEncoded,
apiEndpoint: installShipperSetup?.apiEndpoint,
scriptDownloadUrl:
installShipperSetup?.scriptDownloadUrl,
})}
</EuiCodeBlock>
</EuiSkeletonRectangle>
<EuiCodeBlock language="bash" isCopyable>
{getInstallShipperCommand({
elasticAgentPlatform,
apiKeyEncoded,
apiEndpoint: setup?.apiEndpoint,
scriptDownloadUrl: setup?.scriptDownloadUrl,
})}
</EuiCodeBlock>
</>
),
},
Expand All @@ -165,7 +198,7 @@ export function InstallElasticAgent() {
</EuiText>
<EuiSpacer size="m" />
<EuiSkeletonRectangle
isLoading={yamlConfigStatus === FETCH_STATUS.LOADING}
isLoading={yamlConfigStatus === FETCH_STATUS.NOT_INITIATED}
contentAriaLabel="Elastic agent yaml configuration"
width="100%"
height={300}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@
*/

import { CoreSetup, CoreStart } from '@kbn/core/public';
import type { RouteRepositoryClient } from '@kbn/server-route-repository';
import type {
ReturnOf,
RouteRepositoryClient,
} from '@kbn/server-route-repository';
import { formatRequest } from '@kbn/server-route-repository';
import { FetchOptions } from '../../../common/fetch_options';
import type { ObservabilityOnboardingServerRouteRepository } from '../../../server/routes';
import type {
APIEndpoint,
ObservabilityOnboardingServerRouteRepository,
} from '../../../server/routes';
import { CallApi, callApi } from './call_api';

export type ObservabilityOnboardingClientOptions = Omit<
Expand All @@ -29,6 +35,11 @@ export type AutoAbortedObservabilityClient = RouteRepositoryClient<
Omit<ObservabilityOnboardingClientOptions, 'signal'>
>;

export type APIReturnType<TEndpoint extends APIEndpoint> = ReturnOf<
ObservabilityOnboardingServerRouteRepository,
TEndpoint
>;

export let callObservabilityOnboardingApi: ObservabilityOnboardingClient =
() => {
throw new Error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import { HTTPAuthorizationHeader } from '@kbn/security-plugin/server';

export const getAuthenticationAPIKey = (request: KibanaRequest) => {
const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request);
if (!authorizationHeader) {
throw new Error('Authorization header is missing');
}

// If we don't perform this check we could end up exposing user password because this will return
// something similar to username:password when we are using basic authentication
if (authorizationHeader.scheme.toLowerCase() !== 'apikey') {
return null;
}

if (authorizationHeader && authorizationHeader.credentials) {
const apiKey = Buffer.from(authorizationHeader.credentials, 'base64')
.toString()
Expand All @@ -19,5 +29,4 @@ export const getAuthenticationAPIKey = (request: KibanaRequest) => {
apiKey: apiKey[1],
};
}
throw new Error('Authorization header is missing');
};
Loading