mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Search][Onboarding] api-key plugin (#191926)
## Summary Kibana plugin that helps manage the session for the api-key that provides two exports: - React custom hook to read the api-key stored in session. This hook should return the api-key if it exists, otherwise null. - Component to present either the api key in storage or action to create api key and store into sessionStorage after callback. <img width="1255" alt="Screenshot 2024-09-27 at 20 52 52" src="https://github.com/user-attachments/assets/dc5bcd39-7fe6-433c-8aaa-ad3578a68b62"> <img width="1248" alt="Screenshot 2024-09-27 at 20 52 39" src="https://github.com/user-attachments/assets/d760c163-9017-4f57-ba1a-38ee8ee21534"> <img width="676" alt="Screenshot 2024-09-27 at 20 52 28" src="https://github.com/user-attachments/assets/e908d20a-7e0c-4f3b-9ea2-8e2d1a74c9eb">
This commit is contained in:
parent
d1f24b050b
commit
c5aa739914
52 changed files with 1208 additions and 126 deletions
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
|
@ -753,6 +753,8 @@ src/plugins/screenshot_mode @elastic/appex-sharedux
|
|||
x-pack/examples/screenshotting_example @elastic/appex-sharedux
|
||||
x-pack/plugins/screenshotting @elastic/kibana-reporting-services
|
||||
packages/kbn-screenshotting-server @elastic/appex-sharedux
|
||||
packages/kbn-search-api-keys-components @elastic/search-kibana
|
||||
packages/kbn-search-api-keys-server @elastic/search-kibana
|
||||
packages/kbn-search-api-panels @elastic/search-kibana
|
||||
x-pack/plugins/search_assistant @elastic/search-kibana
|
||||
packages/kbn-search-connectors @elastic/search-kibana
|
||||
|
@ -766,6 +768,7 @@ x-pack/plugins/search_inference_endpoints @elastic/search-kibana
|
|||
x-pack/plugins/search_notebooks @elastic/search-kibana
|
||||
x-pack/plugins/search_playground @elastic/search-kibana
|
||||
packages/kbn-search-response-warnings @elastic/kibana-data-discovery
|
||||
x-pack/packages/search/shared_ui @elastic/search-kibana
|
||||
packages/kbn-search-types @elastic/kibana-data-discovery
|
||||
x-pack/plugins/searchprofiler @elastic/kibana-management
|
||||
x-pack/test/security_api_integration/packages/helpers @elastic/kibana-security
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
"server": "src/legacy/server",
|
||||
"share": ["src/plugins/share", "packages/kbn-reporting-share"],
|
||||
"sharedUXPackages": "packages/shared-ux",
|
||||
"searchApiKeysComponents": "packages/kbn-search-api-keys-components",
|
||||
"searchApiPanels": "packages/kbn-search-api-panels/",
|
||||
"searchErrors": "packages/kbn-search-errors",
|
||||
"searchIndexDocuments": "packages/kbn-search-index-documents",
|
||||
|
|
|
@ -771,6 +771,8 @@
|
|||
"@kbn/screenshotting-example-plugin": "link:x-pack/examples/screenshotting_example",
|
||||
"@kbn/screenshotting-plugin": "link:x-pack/plugins/screenshotting",
|
||||
"@kbn/screenshotting-server": "link:packages/kbn-screenshotting-server",
|
||||
"@kbn/search-api-keys-components": "link:packages/kbn-search-api-keys-components",
|
||||
"@kbn/search-api-keys-server": "link:packages/kbn-search-api-keys-server",
|
||||
"@kbn/search-api-panels": "link:packages/kbn-search-api-panels",
|
||||
"@kbn/search-assistant": "link:x-pack/plugins/search_assistant",
|
||||
"@kbn/search-connectors": "link:packages/kbn-search-connectors",
|
||||
|
@ -784,6 +786,7 @@
|
|||
"@kbn/search-notebooks": "link:x-pack/plugins/search_notebooks",
|
||||
"@kbn/search-playground": "link:x-pack/plugins/search_playground",
|
||||
"@kbn/search-response-warnings": "link:packages/kbn-search-response-warnings",
|
||||
"@kbn/search-shared-ui": "link:x-pack/packages/search/shared_ui",
|
||||
"@kbn/search-types": "link:packages/kbn-search-types",
|
||||
"@kbn/searchprofiler-plugin": "link:x-pack/plugins/searchprofiler",
|
||||
"@kbn/security-api-key-management": "link:x-pack/packages/security/api_key_management",
|
||||
|
|
3
packages/kbn-search-api-keys-components/README.md
Normal file
3
packages/kbn-search-api-keys-components/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Search API Key Components
|
||||
|
||||
The Search API Keys components package is a shared components and utilities to simplify managing the API Keys experience for elasticsearch users across stack and serverless search solutions.
|
13
packages/kbn-search-api-keys-components/index.ts
Normal file
13
packages/kbn-search-api-keys-components/index.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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export * from './src/components/api_key_flyout_wrapper';
|
||||
export * from './src/components/api_key_form';
|
||||
export * from './src/hooks/use_search_api_key';
|
||||
export * from './src/providers/search_api_key_provider';
|
20
packages/kbn-search-api-keys-components/jest.config.js
Normal file
20
packages/kbn-search-api-keys-components/jest.config.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-search-api-keys-components'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/packages/kbn-search-api-keys-components',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/packages/kbn-search-api-keys-components/public/{components,hooks}/**/*.{ts,tsx}',
|
||||
],
|
||||
};
|
5
packages/kbn-search-api-keys-components/kibana.jsonc
Normal file
5
packages/kbn-search-api-keys-components/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/search-api-keys-components",
|
||||
"owner": "@elastic/search-kibana"
|
||||
}
|
6
packages/kbn-search-api-keys-components/package.json
Normal file
6
packages/kbn-search-api-keys-components/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/search-api-keys-components",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ApiKeyFlyout, ApiKeyFlyoutProps } from '@kbn/security-api-key-management';
|
||||
import type { SecurityCreateApiKeyResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
const API_KEY_NAME = 'Unrestricted API Key';
|
||||
|
||||
type ApiKeyFlyoutWrapperProps = Pick<ApiKeyFlyoutProps, 'onCancel'> & {
|
||||
onSuccess?: (createApiKeyResponse: SecurityCreateApiKeyResponse) => void;
|
||||
};
|
||||
|
||||
export const ApiKeyFlyoutWrapper: React.FC<ApiKeyFlyoutWrapperProps> = ({
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
return <ApiKeyFlyout onCancel={onCancel} onSuccess={onSuccess} defaultName={API_KEY_NAME} />;
|
||||
};
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormInfoField } from '@kbn/search-shared-ui';
|
||||
import { ApiKeyFlyoutWrapper } from './api_key_flyout_wrapper';
|
||||
import { useSearchApiKey } from '../hooks/use_search_api_key';
|
||||
import { Status } from '../constants';
|
||||
|
||||
interface ApiKeyFormProps {
|
||||
hasTitle?: boolean;
|
||||
}
|
||||
|
||||
export const ApiKeyForm: React.FC<ApiKeyFormProps> = ({ hasTitle = true }) => {
|
||||
const [showFlyout, setShowFlyout] = useState(false);
|
||||
const { apiKey, status, updateApiKey, toggleApiKeyVisibility, displayedApiKey, apiKeyIsVisible } =
|
||||
useSearchApiKey();
|
||||
|
||||
const titleLocale = i18n.translate('searchApiKeysComponents.apiKeyForm.title', {
|
||||
defaultMessage: 'API Key',
|
||||
});
|
||||
|
||||
if (apiKey && displayedApiKey) {
|
||||
return (
|
||||
<FormInfoField
|
||||
label={hasTitle ? titleLocale : undefined}
|
||||
value={displayedApiKey}
|
||||
copyValue={apiKey}
|
||||
dataTestSubj="apiKeyFormAPIKey"
|
||||
actions={[
|
||||
<EuiButtonIcon
|
||||
iconType={apiKeyIsVisible ? 'eyeClosed' : 'eye'}
|
||||
color="success"
|
||||
onClick={toggleApiKeyVisibility}
|
||||
data-test-subj="showAPIKeyButton"
|
||||
aria-label={i18n.translate('searchApiKeysComponents.apiKeyForm.showApiKey', {
|
||||
defaultMessage: 'Show API Key',
|
||||
})}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexStart" responsive={false}>
|
||||
{hasTitle && (
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiTitle size="xxxs" css={{ whiteSpace: 'nowrap' }}>
|
||||
<h6>{titleLocale}</h6>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{status === Status.showUserPrivilegesError && (
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiBadge data-test-subj="apiKeyFormNoUserPrivileges">
|
||||
{i18n.translate('searchApiKeysComponents.apiKeyForm.noUserPrivileges', {
|
||||
defaultMessage: "You don't have access to manage API keys",
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{status === Status.showCreateButton && (
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
size="s"
|
||||
iconSide="left"
|
||||
iconType="key"
|
||||
onClick={() => setShowFlyout(true)}
|
||||
data-test-subj="createAPIKeyButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="searchApiKeysComponents.apiKeyForm.createButton"
|
||||
defaultMessage="Create an API Key"
|
||||
/>
|
||||
</EuiButton>
|
||||
{showFlyout && (
|
||||
<ApiKeyFlyoutWrapper onCancel={() => setShowFlyout(false)} onSuccess={updateApiKey} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
17
packages/kbn-search-api-keys-components/src/constants.ts
Normal file
17
packages/kbn-search-api-keys-components/src/constants.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export enum Status {
|
||||
uninitialized = 'uninitialized',
|
||||
loading = 'loading',
|
||||
showCreateButton = 'showCreateButton',
|
||||
showHiddenKey = 'showHiddenKey',
|
||||
showPreviewKey = 'showPreviewKey',
|
||||
showUserPrivilegesError = 'showUserPrivilegesError',
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { ApiKeyContext } from '../providers/search_api_key_provider';
|
||||
|
||||
export const useSearchApiKey = () => {
|
||||
const { initialiseKey, ...context } = useContext(ApiKeyContext);
|
||||
useEffect(() => {
|
||||
initialiseKey();
|
||||
}, [initialiseKey]);
|
||||
return context;
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useCallback, useReducer, createContext, useEffect } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { APIKeyCreationResponse } from '@kbn/search-api-keys-server/types';
|
||||
import { APIRoutes } from '../types';
|
||||
import { Status } from '../constants';
|
||||
|
||||
const API_KEY_STORAGE_KEY = 'searchApiKey';
|
||||
const API_KEY_MASK = '•'.repeat(60);
|
||||
|
||||
interface ApiKeyState {
|
||||
status: Status;
|
||||
apiKey: string | null;
|
||||
}
|
||||
|
||||
interface APIKeyContext {
|
||||
displayedApiKey: string | null;
|
||||
apiKey: string | null;
|
||||
toggleApiKeyVisibility: () => void;
|
||||
updateApiKey: ({ id, encoded }: { id: string; encoded: string }) => void;
|
||||
status: Status;
|
||||
apiKeyIsVisible: boolean;
|
||||
initialiseKey: () => void;
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'SET_API_KEY'; apiKey: string; status: Status }
|
||||
| { type: 'SET_STATUS'; status: Status }
|
||||
| { type: 'CLEAR_API_KEY' }
|
||||
| { type: 'TOGGLE_API_KEY_VISIBILITY' };
|
||||
|
||||
const initialState: ApiKeyState = {
|
||||
apiKey: null,
|
||||
status: Status.uninitialized,
|
||||
};
|
||||
|
||||
const reducer = (state: ApiKeyState, action: Action): ApiKeyState => {
|
||||
switch (action.type) {
|
||||
case 'SET_API_KEY':
|
||||
return { ...state, apiKey: action.apiKey, status: action.status };
|
||||
case 'SET_STATUS':
|
||||
return { ...state, status: action.status };
|
||||
case 'TOGGLE_API_KEY_VISIBILITY':
|
||||
return {
|
||||
...state,
|
||||
status:
|
||||
state.status === Status.showHiddenKey ? Status.showPreviewKey : Status.showHiddenKey,
|
||||
};
|
||||
case 'CLEAR_API_KEY':
|
||||
return { ...state, apiKey: null, status: Status.showCreateButton };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const ApiKeyContext = createContext<APIKeyContext>({
|
||||
displayedApiKey: null,
|
||||
apiKey: null,
|
||||
toggleApiKeyVisibility: () => {},
|
||||
updateApiKey: () => {},
|
||||
status: Status.uninitialized,
|
||||
apiKeyIsVisible: false,
|
||||
initialiseKey: () => {},
|
||||
});
|
||||
|
||||
export const SearchApiKeyProvider: React.FC = ({ children }) => {
|
||||
const { http } = useKibana().services;
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const updateApiKey = useCallback(({ id, encoded }: { id: string; encoded: string }) => {
|
||||
sessionStorage.setItem(API_KEY_STORAGE_KEY, JSON.stringify({ id, encoded }));
|
||||
dispatch({ type: 'SET_API_KEY', apiKey: encoded, status: Status.showHiddenKey });
|
||||
}, []);
|
||||
const handleShowKeyVisibility = useCallback(() => {
|
||||
dispatch({ type: 'TOGGLE_API_KEY_VISIBILITY' });
|
||||
}, []);
|
||||
const initialiseKey = useCallback(() => {
|
||||
dispatch({ type: 'SET_STATUS', status: Status.loading });
|
||||
}, []);
|
||||
const { mutateAsync: validateApiKey } = useMutation(async (id: string) => {
|
||||
try {
|
||||
if (!http?.post) {
|
||||
throw new Error('HTTP service is unavailable');
|
||||
}
|
||||
|
||||
const response = await http.post<{ isValid: boolean }>(APIRoutes.API_KEY_VALIDITY, {
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
|
||||
return response.isValid;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const { mutateAsync: createApiKey } = useMutation<APIKeyCreationResponse | undefined>({
|
||||
mutationFn: async () => {
|
||||
try {
|
||||
if (!http?.post) {
|
||||
throw new Error('HTTP service is unavailable');
|
||||
}
|
||||
|
||||
return await http.post<APIKeyCreationResponse>(APIRoutes.API_KEYS);
|
||||
} catch (err) {
|
||||
if (err.response?.status === 400) {
|
||||
dispatch({ type: 'SET_STATUS', status: Status.showCreateButton });
|
||||
} else if (err.response?.status === 403) {
|
||||
dispatch({ type: 'SET_STATUS', status: Status.showUserPrivilegesError });
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess: (receivedApiKey) => {
|
||||
if (receivedApiKey) {
|
||||
sessionStorage.setItem(
|
||||
API_KEY_STORAGE_KEY,
|
||||
JSON.stringify({ id: receivedApiKey.id, encoded: receivedApiKey.encoded })
|
||||
);
|
||||
dispatch({
|
||||
type: 'SET_API_KEY',
|
||||
apiKey: receivedApiKey.encoded,
|
||||
status: Status.showHiddenKey,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const initialiseApiKey = async () => {
|
||||
try {
|
||||
if (state.status === Status.loading) {
|
||||
const storedKey = sessionStorage.getItem(API_KEY_STORAGE_KEY);
|
||||
|
||||
if (storedKey) {
|
||||
const { id, encoded } = JSON.parse(storedKey);
|
||||
|
||||
if (await validateApiKey(id)) {
|
||||
dispatch({
|
||||
type: 'SET_API_KEY',
|
||||
apiKey: encoded,
|
||||
status: Status.showHiddenKey,
|
||||
});
|
||||
} else {
|
||||
sessionStorage.removeItem(API_KEY_STORAGE_KEY);
|
||||
dispatch({
|
||||
type: 'CLEAR_API_KEY',
|
||||
});
|
||||
await createApiKey();
|
||||
}
|
||||
} else {
|
||||
await createApiKey();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
dispatch({ type: 'CLEAR_API_KEY' });
|
||||
}
|
||||
};
|
||||
|
||||
initialiseApiKey();
|
||||
}, [state.status, createApiKey, validateApiKey]);
|
||||
|
||||
const value: APIKeyContext = {
|
||||
displayedApiKey: state.status === Status.showHiddenKey ? API_KEY_MASK : state.apiKey,
|
||||
apiKey: state.apiKey,
|
||||
toggleApiKeyVisibility: handleShowKeyVisibility,
|
||||
updateApiKey,
|
||||
status: state.status,
|
||||
apiKeyIsVisible: state.status === Status.showPreviewKey,
|
||||
initialiseKey,
|
||||
};
|
||||
|
||||
return <ApiKeyContext.Provider value={value}>{children}</ApiKeyContext.Provider>;
|
||||
};
|
13
packages/kbn-search-api-keys-components/src/types.ts
Normal file
13
packages/kbn-search-api-keys-components/src/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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export enum APIRoutes {
|
||||
API_KEYS = '/internal/search_api_keys',
|
||||
API_KEY_VALIDITY = '/internal/search_api_keys/validity',
|
||||
}
|
21
packages/kbn-search-api-keys-components/tsconfig.json
Normal file
21
packages/kbn-search-api-keys-components/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"index.ts",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/i18n",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/security-api-key-management",
|
||||
"@kbn/search-shared-ui",
|
||||
"@kbn/search-api-keys-server"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
}
|
3
packages/kbn-search-api-keys-server/README.md
Normal file
3
packages/kbn-search-api-keys-server/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Search API Keys
|
||||
|
||||
The Search API Keys package is a shared components and utilities to simplify managing the API Keys experience for elasticsearch users across stack and serverless search solutions.
|
10
packages/kbn-search-api-keys-server/index.ts
Normal file
10
packages/kbn-search-api-keys-server/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export * from './src/routes/routes';
|
17
packages/kbn-search-api-keys-server/jest.config.js
Normal file
17
packages/kbn-search-api-keys-server/jest.config.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-search-api-keys-server'],
|
||||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/packages/kbn-search-api-keys-server',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: ['<rootDir>/packages/kbn-search-api-keys-server/**/*.{ts,tsx}'],
|
||||
};
|
5
packages/kbn-search-api-keys-server/kibana.jsonc
Normal file
5
packages/kbn-search-api-keys-server/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-server",
|
||||
"id": "@kbn/search-api-keys-server",
|
||||
"owner": "@elastic/search-kibana"
|
||||
}
|
6
packages/kbn-search-api-keys-server/package.json
Normal file
6
packages/kbn-search-api-keys-server/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/search-api-keys-server",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
31
packages/kbn-search-api-keys-server/src/lib/create_key.ts
Normal file
31
packages/kbn-search-api-keys-server/src/lib/create_key.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { APIKeyCreationResponse } from '../../types';
|
||||
|
||||
export async function createAPIKey(
|
||||
name: string,
|
||||
client: ElasticsearchClient,
|
||||
logger: Logger
|
||||
): Promise<APIKeyCreationResponse> {
|
||||
try {
|
||||
const apiKey = await client.security.createApiKey({
|
||||
name,
|
||||
role_descriptors: {},
|
||||
});
|
||||
|
||||
return apiKey;
|
||||
} catch (e) {
|
||||
logger.error(`Search API Keys: Error during creating API Key`);
|
||||
logger.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
30
packages/kbn-search-api-keys-server/src/lib/get_key_by_id.ts
Normal file
30
packages/kbn-search-api-keys-server/src/lib/get_key_by_id.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { GetApiKeyResponse } from '../../types';
|
||||
|
||||
export async function getAPIKeyById(
|
||||
id: string,
|
||||
client: ElasticsearchClient,
|
||||
logger: Logger
|
||||
): Promise<GetApiKeyResponse> {
|
||||
try {
|
||||
const apiKey = await client.security.getApiKey({
|
||||
id,
|
||||
});
|
||||
|
||||
return apiKey.api_keys?.[0];
|
||||
} catch (e) {
|
||||
logger.error(`Search API Keys: Error on getting API Key`);
|
||||
logger.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
50
packages/kbn-search-api-keys-server/src/lib/privileges.ts
Normal file
50
packages/kbn-search-api-keys-server/src/lib/privileges.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
export async function fetchUserStartPrivileges(
|
||||
client: ElasticsearchClient,
|
||||
logger: Logger
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// relying on manage cluster privilege to check if user can create API keys
|
||||
// and can also have permissions for index monitoring
|
||||
const securityCheck = await client.security.hasPrivileges({
|
||||
cluster: ['manage'],
|
||||
});
|
||||
|
||||
return securityCheck?.cluster?.manage ?? false;
|
||||
} catch (e) {
|
||||
logger.error(`Error checking user privileges for search API Keys`);
|
||||
logger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchClusterHasApiKeys(
|
||||
client: ElasticsearchClient,
|
||||
logger: Logger
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const clusterApiKeys = await client.security.queryApiKeys({
|
||||
query: {
|
||||
term: {
|
||||
invalidated: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
return clusterApiKeys.api_keys.length > 0;
|
||||
} catch (e) {
|
||||
logger.error(`Error checking cluster for existing valid API keys`);
|
||||
logger.error(e);
|
||||
return true;
|
||||
}
|
||||
}
|
108
packages/kbn-search-api-keys-server/src/routes/routes.ts
Normal file
108
packages/kbn-search-api-keys-server/src/routes/routes.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { APIRoutes } from '../../types';
|
||||
import { getAPIKeyById } from '../lib/get_key_by_id';
|
||||
import { createAPIKey } from '../lib/create_key';
|
||||
import { fetchClusterHasApiKeys, fetchUserStartPrivileges } from '../lib/privileges';
|
||||
|
||||
const API_KEY_NAME = 'Unrestricted API Key';
|
||||
|
||||
export function registerSearchApiKeysRoutes(router: IRouter, logger: Logger) {
|
||||
router.post(
|
||||
{
|
||||
path: APIRoutes.API_KEY_VALIDITY,
|
||||
validate: {
|
||||
body: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
},
|
||||
options: {
|
||||
access: 'internal',
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
try {
|
||||
const core = await context.core;
|
||||
const client = core.elasticsearch.client.asCurrentUser;
|
||||
const apiKey = await getAPIKeyById(request.body.id, client, logger);
|
||||
|
||||
if (!apiKey) {
|
||||
return response.customError({
|
||||
body: { message: 'API key is not found.' },
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return response.ok({
|
||||
body: { isValid: !apiKey.invalidated },
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Error fetching API Key`);
|
||||
logger.error(e);
|
||||
return response.customError({
|
||||
body: { message: e.message },
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: APIRoutes.API_KEYS,
|
||||
validate: {},
|
||||
options: {
|
||||
access: 'internal',
|
||||
},
|
||||
},
|
||||
async (context, _request, response) => {
|
||||
try {
|
||||
const core = await context.core;
|
||||
const client = core.elasticsearch.client.asCurrentUser;
|
||||
const clusterHasApiKeys = await fetchClusterHasApiKeys(client, logger);
|
||||
|
||||
if (clusterHasApiKeys) {
|
||||
return response.customError({
|
||||
body: { message: 'Project already has API keys' },
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const canCreateApiKeys = await fetchUserStartPrivileges(client, logger);
|
||||
|
||||
if (!canCreateApiKeys) {
|
||||
return response.customError({
|
||||
body: { message: 'User does not have required privileges' },
|
||||
statusCode: 403,
|
||||
});
|
||||
}
|
||||
|
||||
const apiKey = await createAPIKey(API_KEY_NAME, client, logger);
|
||||
|
||||
return response.ok({
|
||||
body: apiKey,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Error creating API Key`);
|
||||
logger.error(e);
|
||||
return response.customError({
|
||||
body: { message: e.message },
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
20
packages/kbn-search-api-keys-server/tsconfig.json
Normal file
20
packages/kbn-search-api-keys-server/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"types.ts",
|
||||
"index.ts"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core-elasticsearch-server",
|
||||
"@kbn/logging",
|
||||
"@kbn/core",
|
||||
"@kbn/config-schema",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
}
|
27
packages/kbn-search-api-keys-server/types.ts
Normal file
27
packages/kbn-search-api-keys-server/types.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export enum APIRoutes {
|
||||
API_KEYS = '/internal/search_api_keys',
|
||||
API_KEY_VALIDITY = '/internal/search_api_keys/validity',
|
||||
}
|
||||
|
||||
export interface APIKey {
|
||||
id: string;
|
||||
name: string;
|
||||
expiration?: number;
|
||||
invalidated?: boolean;
|
||||
}
|
||||
|
||||
export interface APIKeyCreationResponse extends Pick<APIKey, 'id' | 'name' | 'expiration'> {
|
||||
api_key: string;
|
||||
encoded: string;
|
||||
}
|
||||
|
||||
export type GetApiKeyResponse = APIKey;
|
|
@ -1500,6 +1500,10 @@
|
|||
"@kbn/screenshotting-plugin/*": ["x-pack/plugins/screenshotting/*"],
|
||||
"@kbn/screenshotting-server": ["packages/kbn-screenshotting-server"],
|
||||
"@kbn/screenshotting-server/*": ["packages/kbn-screenshotting-server/*"],
|
||||
"@kbn/search-api-keys-components": ["packages/kbn-search-api-keys-components"],
|
||||
"@kbn/search-api-keys-components/*": ["packages/kbn-search-api-keys-components/*"],
|
||||
"@kbn/search-api-keys-server": ["packages/kbn-search-api-keys-server"],
|
||||
"@kbn/search-api-keys-server/*": ["packages/kbn-search-api-keys-server/*"],
|
||||
"@kbn/search-api-panels": ["packages/kbn-search-api-panels"],
|
||||
"@kbn/search-api-panels/*": ["packages/kbn-search-api-panels/*"],
|
||||
"@kbn/search-assistant": ["x-pack/plugins/search_assistant"],
|
||||
|
@ -1526,6 +1530,8 @@
|
|||
"@kbn/search-playground/*": ["x-pack/plugins/search_playground/*"],
|
||||
"@kbn/search-response-warnings": ["packages/kbn-search-response-warnings"],
|
||||
"@kbn/search-response-warnings/*": ["packages/kbn-search-response-warnings/*"],
|
||||
"@kbn/search-shared-ui": ["x-pack/packages/search/shared_ui"],
|
||||
"@kbn/search-shared-ui/*": ["x-pack/packages/search/shared_ui/*"],
|
||||
"@kbn/search-types": ["packages/kbn-search-types"],
|
||||
"@kbn/search-types/*": ["packages/kbn-search-types/*"],
|
||||
"@kbn/searchprofiler-plugin": ["x-pack/plugins/searchprofiler"],
|
||||
|
@ -1996,7 +2002,9 @@
|
|||
"@kbn/zod-helpers/*": ["packages/kbn-zod-helpers/*"],
|
||||
// END AUTOMATED PACKAGE LISTING
|
||||
// Allows for importing from `kibana` package for the exported types.
|
||||
"@emotion/core": ["typings/@emotion"]
|
||||
"@emotion/core": [
|
||||
"typings/@emotion"
|
||||
]
|
||||
},
|
||||
// Support .tsx files and transform JSX into calls to React.createElement
|
||||
"jsx": "react",
|
||||
|
@ -2070,4 +2078,4 @@
|
|||
"@kbn/ambient-storybook-types"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -104,6 +104,7 @@
|
|||
"xpack.rollupJobs": ["packages/rollup", "plugins/rollup"],
|
||||
"xpack.runtimeFields": "plugins/runtime_fields",
|
||||
"xpack.screenshotting": "plugins/screenshotting",
|
||||
"xpack.searchSharedUI": "packages/search/shared_ui",
|
||||
"xpack.searchHomepage": "plugins/search_homepage",
|
||||
"xpack.searchIndices": "plugins/search_indices",
|
||||
"xpack.searchNotebooks": "plugins/search_notebooks",
|
||||
|
|
3
x-pack/packages/search/shared_ui/README.md
Normal file
3
x-pack/packages/search/shared_ui/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/search-shared-ui
|
||||
|
||||
Contains form components used within search indices plugin.
|
8
x-pack/packages/search/shared_ui/index.ts
Normal file
8
x-pack/packages/search/shared_ui/index.ts
Normal file
|
@ -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 * from './src/form_info_field/form_info_field';
|
15
x-pack/packages/search/shared_ui/jest.config.js
Normal file
15
x-pack/packages/search/shared_ui/jest.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/packages/search/shared_ui',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: ['<rootDir>/x-pack/packages/search/shared_ui/**/*.{ts,tsx}'],
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../..',
|
||||
roots: ['<rootDir>/x-pack/packages/search/shared_ui'],
|
||||
};
|
5
x-pack/packages/search/shared_ui/kibana.jsonc
Normal file
5
x-pack/packages/search/shared_ui/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/search-shared-ui",
|
||||
"owner": "@elastic/search-kibana"
|
||||
}
|
6
x-pack/packages/search/shared_ui/package.json
Normal file
6
x-pack/packages/search/shared_ui/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/search-shared-ui",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0"
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiCopy,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface FormInfoFieldProps {
|
||||
actions?: React.ReactNode[];
|
||||
label?: string;
|
||||
value: string;
|
||||
copyValue?: string;
|
||||
dataTestSubj?: string;
|
||||
}
|
||||
|
||||
export const FormInfoField: React.FC<FormInfoFieldProps> = ({
|
||||
actions = [],
|
||||
label,
|
||||
value,
|
||||
copyValue,
|
||||
dataTestSubj,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{label && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxxs">
|
||||
<h1>{label}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={0} css={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
|
||||
<code
|
||||
data-test-subj={dataTestSubj}
|
||||
style={{
|
||||
color: euiTheme.colors.successText,
|
||||
padding: `${euiTheme.size.s} ${euiTheme.size.m}`,
|
||||
backgroundColor: euiTheme.colors.lightestShade,
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
borderRadius: euiTheme.border.radius.small,
|
||||
fontWeight: euiTheme.font.weight.bold,
|
||||
fontSize: euiTheme.size.m,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</code>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCopy
|
||||
textToCopy={copyValue ?? value}
|
||||
afterMessage={i18n.translate('xpack.searchSharedUI.formInfoField.copyMessage', {
|
||||
defaultMessage: 'Copied',
|
||||
})}
|
||||
>
|
||||
{(copy) => (
|
||||
<EuiButtonIcon
|
||||
onClick={copy}
|
||||
iconType="copy"
|
||||
aria-label={i18n.translate('xpack.searchSharedUI.formInfoField.copyMessage', {
|
||||
defaultMessage: 'Copy to clipboard',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiFlexItem>
|
||||
{actions.map((action, index) => (
|
||||
<EuiFlexItem key={index} grow={false}>
|
||||
{action}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
22
x-pack/packages/search/shared_ui/tsconfig.json
Normal file
22
x-pack/packages/search/shared_ui/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react",
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/i18n",
|
||||
],
|
||||
}
|
|
@ -96,6 +96,7 @@ interface CommonApiKeyFlyoutProps {
|
|||
http?: CoreStart['http'];
|
||||
currentUser?: AuthenticatedUser;
|
||||
isLoadingCurrentUser?: boolean;
|
||||
defaultName?: string;
|
||||
defaultMetadata?: string;
|
||||
defaultRoleDescriptors?: string;
|
||||
defaultExpiration?: string;
|
||||
|
@ -172,6 +173,7 @@ export const ApiKeyFlyout: FunctionComponent<ApiKeyFlyoutProps> = ({
|
|||
defaultExpiration,
|
||||
defaultMetadata,
|
||||
defaultRoleDescriptors,
|
||||
defaultName,
|
||||
apiKey,
|
||||
canManageCrossClusterApiKeys = false,
|
||||
readOnly = false,
|
||||
|
@ -250,6 +252,12 @@ export const ApiKeyFlyout: FunctionComponent<ApiKeyFlyoutProps> = ({
|
|||
}
|
||||
}, [defaultRoleDescriptors]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultName && !apiKey) {
|
||||
formik.setFieldValue('name', defaultName);
|
||||
}
|
||||
}, [defaultName]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultMetadata && !apiKey) {
|
||||
formik.setFieldValue('metadata', defaultMetadata);
|
||||
|
|
|
@ -24,4 +24,4 @@
|
|||
"esUiShared"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
|||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { SearchApiKeyProvider } from '@kbn/search-api-keys-components';
|
||||
import { UsageTrackerContextProvider } from './contexts/usage_tracker_context';
|
||||
import { SearchIndicesServicesContextDeps } from './types';
|
||||
|
||||
|
@ -29,7 +30,9 @@ export const renderApp = async (
|
|||
<UsageTrackerContextProvider usageCollection={services.usageCollection}>
|
||||
<I18nProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<SearchApiKeyProvider>
|
||||
<App />
|
||||
</SearchApiKeyProvider>
|
||||
</QueryClientProvider>
|
||||
</I18nProvider>
|
||||
</UsageTrackerContextProvider>
|
||||
|
|
|
@ -7,67 +7,22 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiCopy,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { FormInfoField } from '@kbn/search-shared-ui';
|
||||
import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url';
|
||||
|
||||
export const ConnectionDetails: React.FC = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const elasticsearchUrl = useElasticsearchUrl();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxxs">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.searchIndices.connectionDetails.endpointTitle"
|
||||
defaultMessage="Elasticsearch URL"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem css={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
|
||||
<p
|
||||
data-test-subj="connectionDetailsEndpoint"
|
||||
css={{
|
||||
color: euiTheme.colors.successText,
|
||||
padding: `${euiTheme.size.s} ${euiTheme.size.m}`,
|
||||
backgroundColor: euiTheme.colors.lightestShade,
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{elasticsearchUrl}
|
||||
</p>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCopy
|
||||
textToCopy={elasticsearchUrl}
|
||||
afterMessage={i18n.translate('xpack.searchIndices.connectionDetails.copyMessage', {
|
||||
defaultMessage: 'Copied',
|
||||
})}
|
||||
>
|
||||
{(copy) => (
|
||||
<EuiButtonIcon
|
||||
onClick={copy}
|
||||
iconType="copy"
|
||||
aria-label={i18n.translate('xpack.searchIndices.connectionDetails.copyMessage', {
|
||||
defaultMessage: 'Copy Elasticsearch URL to clipboard',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<FormInfoField
|
||||
label={i18n.translate('xpack.searchIndices.connectionDetails.endpointTitle', {
|
||||
defaultMessage: 'Elasticsearch URL',
|
||||
})}
|
||||
value={elasticsearchUrl}
|
||||
copyValue={elasticsearchUrl}
|
||||
dataTestSubj="connectionDetailsEndpoint"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { TryInConsoleButton } from '@kbn/try-in-console';
|
||||
|
||||
import { useSearchApiKey } from '@kbn/search-api-keys-components';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { IngestCodeSnippetParameters } from '../../types';
|
||||
import { LanguageSelector } from '../shared/language_selector';
|
||||
|
@ -58,6 +59,7 @@ export const AddDocumentsCodeExample = ({
|
|||
// TODO: implement smart document generation
|
||||
return generateSampleDocument(codeSampleMappings);
|
||||
}, [codeSampleMappings]);
|
||||
const { apiKey, apiKeyIsVisible } = useSearchApiKey();
|
||||
const codeParams: IngestCodeSnippetParameters = useMemo(() => {
|
||||
return {
|
||||
indexName,
|
||||
|
@ -65,8 +67,17 @@ export const AddDocumentsCodeExample = ({
|
|||
sampleDocument,
|
||||
indexHasMappings,
|
||||
mappingProperties: codeSampleMappings,
|
||||
apiKey: apiKeyIsVisible && apiKey ? apiKey : undefined,
|
||||
};
|
||||
}, [indexName, elasticsearchUrl, sampleDocument, codeSampleMappings, indexHasMappings]);
|
||||
}, [
|
||||
indexName,
|
||||
elasticsearchUrl,
|
||||
sampleDocument,
|
||||
codeSampleMappings,
|
||||
indexHasMappings,
|
||||
apiKeyIsVisible,
|
||||
apiKey,
|
||||
]);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
|
|
|
@ -26,6 +26,7 @@ import { useParams } from 'react-router-dom';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SectionLoading } from '@kbn/es-ui-shared-plugin/public';
|
||||
import { ApiKeyForm } from '@kbn/search-api-keys-components';
|
||||
import { useIndex } from '../../hooks/api/use_index';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { ConnectionDetails } from '../connection_details/connection_details';
|
||||
|
@ -228,11 +229,13 @@ export const SearchIndexDetailsPage = () => {
|
|||
<EuiPageTemplate.Section grow={false}>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup css={{ overflow: 'auto' }}>
|
||||
<EuiFlexItem css={{ flexShrink: 0 }}>
|
||||
<ConnectionDetails />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{/* TODO: API KEY */}</EuiFlexItem>
|
||||
<EuiFlexItem css={{ flexShrink: 0 }}>
|
||||
<ApiKeyForm />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TryInConsoleButton } from '@kbn/try-in-console';
|
||||
|
||||
import { ApiKeyForm, useSearchApiKey } from '@kbn/search-api-keys-components';
|
||||
import { AnalyticsEvents } from '../../analytics/constants';
|
||||
import { Languages, AvailableLanguages, LanguageOptions } from '../../code_examples';
|
||||
|
||||
|
@ -47,18 +48,48 @@ export const CreateIndexCodeView = ({
|
|||
[usageTracker, changeCodingLanguage]
|
||||
);
|
||||
const elasticsearchUrl = useElasticsearchUrl();
|
||||
const { apiKey, apiKeyIsVisible } = useSearchApiKey();
|
||||
|
||||
const codeParams = useMemo(() => {
|
||||
return {
|
||||
indexName: createIndexForm.indexName || undefined,
|
||||
elasticsearchURL: elasticsearchUrl,
|
||||
apiKey: apiKeyIsVisible && apiKey ? apiKey : undefined,
|
||||
};
|
||||
}, [createIndexForm.indexName, elasticsearchUrl]);
|
||||
}, [createIndexForm.indexName, elasticsearchUrl, apiKeyIsVisible, apiKey]);
|
||||
const selectedCodeExample = useMemo(() => {
|
||||
return selectedCodeExamples[selectedLanguage];
|
||||
}, [selectedLanguage, selectedCodeExamples]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" data-test-subj="createIndexCodeView">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiPanel paddingSize="m" hasShadow={false} hasBorder={true} color="plain">
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h5>
|
||||
{i18n.translate('xpack.searchIndices.startPage.codeView.apiKeyTitle', {
|
||||
defaultMessage: 'Copy your API key',
|
||||
})}
|
||||
</h5>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
{i18n.translate('xpack.searchIndices.startPage.codeView.apiKeyDescription', {
|
||||
defaultMessage:
|
||||
'Make sure you keep it somewhere safe. You won’t be able to retrieve it later.',
|
||||
})}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<ApiKeyForm hasTitle={false} />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem css={{ maxWidth: '300px' }}>
|
||||
<LanguageSelector
|
||||
|
@ -94,6 +125,7 @@ export const CreateIndexCodeView = ({
|
|||
/>
|
||||
)}
|
||||
<CodeSample
|
||||
id="createIndex"
|
||||
title={i18n.translate('xpack.searchIndices.startPage.codeView.createIndex.title', {
|
||||
defaultMessage: 'Connect and create an index',
|
||||
})}
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
import type { IRouter } from '@kbn/core/server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
import { registerSearchApiKeysRoutes } from '@kbn/search-api-keys-server';
|
||||
import { registerIndicesRoutes } from './indices';
|
||||
import { registerStatusRoutes } from './status';
|
||||
|
||||
export function defineRoutes(router: IRouter, logger: Logger) {
|
||||
registerIndicesRoutes(router, logger);
|
||||
registerStatusRoutes(router, logger);
|
||||
registerSearchApiKeysRoutes(router, logger);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,9 @@
|
|||
"@kbn/cloud-plugin",
|
||||
"@kbn/search-index-documents",
|
||||
"@kbn/es-types",
|
||||
"@kbn/search-api-keys-server",
|
||||
"@kbn/search-api-keys-components",
|
||||
"@kbn/search-shared-ui"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -172,44 +172,6 @@ export function defineRoutes({
|
|||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: APIRoutes.POST_API_KEY,
|
||||
validate: {
|
||||
body: schema.object({
|
||||
name: schema.string(),
|
||||
expiresInDays: schema.number(),
|
||||
indices: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
errorHandler(logger)(async (context, request, response) => {
|
||||
const { name, expiresInDays, indices } = request.body;
|
||||
const { client } = (await context.core).elasticsearch;
|
||||
|
||||
const apiKey = await client.asCurrentUser.security.createApiKey({
|
||||
name,
|
||||
expiration: `${expiresInDays}d`,
|
||||
role_descriptors: {
|
||||
[`playground-${name}-role`]: {
|
||||
cluster: [],
|
||||
indices: [
|
||||
{
|
||||
names: indices,
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
body: { apiKey },
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// SECURITY: We don't apply any authorization tags to this route because all actions performed
|
||||
// on behalf of the user making the request and governed by the user's own cluster privileges.
|
||||
router.get(
|
||||
|
|
|
@ -23,6 +23,7 @@ import { SvlIngestPipelines } from './svl_ingest_pipelines';
|
|||
import { SvlSearchHomePageProvider } from './svl_search_homepage';
|
||||
import { SvlSearchIndexDetailPageProvider } from './svl_search_index_detail_page';
|
||||
import { SvlSearchElasticsearchStartPageProvider } from './svl_search_elasticsearch_start_page';
|
||||
import { SvlApiKeysProvider } from './svl_api_keys';
|
||||
|
||||
export const pageObjects = {
|
||||
...xpackFunctionalPageObjects,
|
||||
|
@ -43,4 +44,5 @@ export const pageObjects = {
|
|||
svlSearchHomePage: SvlSearchHomePageProvider,
|
||||
svlSearchIndexDetailPage: SvlSearchIndexDetailPageProvider,
|
||||
svlSearchElasticsearchStartPage: SvlSearchElasticsearchStartPageProvider,
|
||||
svlApiKeys: SvlApiKeysProvider,
|
||||
};
|
||||
|
|
108
x-pack/test_serverless/functional/page_objects/svl_api_keys.ts
Normal file
108
x-pack/test_serverless/functional/page_objects/svl_api_keys.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
const APIKEY_MASK = '•'.repeat(60);
|
||||
|
||||
export function SvlApiKeysProvider({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const browser = getService('browser');
|
||||
const pageObjects = getPageObjects(['common', 'apiKeys']);
|
||||
const retry = getService('retry');
|
||||
const es = getService('es');
|
||||
|
||||
const getAPIKeyFromSessionStorage = async () => {
|
||||
const sessionStorageKey = await browser.getSessionStorageItem('searchApiKey');
|
||||
return sessionStorageKey && JSON.parse(sessionStorageKey);
|
||||
};
|
||||
|
||||
return {
|
||||
async clearAPIKeySessionStorage() {
|
||||
await browser.clearSessionStorage();
|
||||
},
|
||||
|
||||
async expectAPIKeyAvailable() {
|
||||
await testSubjects.existOrFail('apiKeyFormAPIKey');
|
||||
await retry.try(async () => {
|
||||
expect(await testSubjects.getVisibleText('apiKeyFormAPIKey')).to.be(APIKEY_MASK);
|
||||
});
|
||||
await testSubjects.click('showAPIKeyButton');
|
||||
let apiKey;
|
||||
await retry.try(async () => {
|
||||
apiKey = await testSubjects.getVisibleText('apiKeyFormAPIKey');
|
||||
expect(apiKey).to.be.a('string');
|
||||
expect(apiKey.length).to.be(60);
|
||||
expect(apiKey).to.not.be(APIKEY_MASK);
|
||||
});
|
||||
const sessionStorageKey = await getAPIKeyFromSessionStorage();
|
||||
expect(sessionStorageKey.encoded).to.eql(apiKey);
|
||||
},
|
||||
|
||||
async getAPIKeyFromSessionStorage() {
|
||||
return getAPIKeyFromSessionStorage();
|
||||
},
|
||||
|
||||
async getAPIKeyFromUI() {
|
||||
let apiKey = '';
|
||||
await retry.try(async () => {
|
||||
apiKey = await testSubjects.getVisibleText('apiKeyFormAPIKey');
|
||||
expect(apiKey).to.not.be(APIKEY_MASK);
|
||||
});
|
||||
expect(apiKey).to.be.a('string');
|
||||
return apiKey;
|
||||
},
|
||||
|
||||
async invalidateAPIKey(apiKeyId: string) {
|
||||
await es.security.invalidateApiKey({ ids: [apiKeyId] });
|
||||
},
|
||||
|
||||
async createAPIKey() {
|
||||
await es.security.createApiKey({
|
||||
name: 'test-api-key',
|
||||
role_descriptors: {},
|
||||
});
|
||||
},
|
||||
|
||||
async expectAPIKeyCreate() {
|
||||
await testSubjects.existOrFail('apiKeyFormAPIKey');
|
||||
await retry.try(async () => {
|
||||
expect(await testSubjects.getVisibleText('apiKeyFormAPIKey')).to.be(APIKEY_MASK);
|
||||
});
|
||||
await testSubjects.click('showAPIKeyButton');
|
||||
await retry.try(async () => {
|
||||
const apiKey = await testSubjects.getVisibleText('apiKeyFormAPIKey');
|
||||
expect(apiKey).to.be.a('string');
|
||||
expect(apiKey.length).to.be(60);
|
||||
expect(apiKey).to.not.be(APIKEY_MASK);
|
||||
});
|
||||
},
|
||||
|
||||
async deleteAPIKeys() {
|
||||
const { api_keys: apiKeys } = await es.security.getApiKey();
|
||||
await es.security.invalidateApiKey({ ids: apiKeys.map((key) => key.id) });
|
||||
},
|
||||
|
||||
async expectCreateApiKeyAction() {
|
||||
await testSubjects.existOrFail('createAPIKeyButton');
|
||||
},
|
||||
|
||||
async createApiKeyFromFlyout() {
|
||||
const apiKeyName = 'Happy API Key';
|
||||
await testSubjects.click('createAPIKeyButton');
|
||||
expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('Create API key');
|
||||
|
||||
await pageObjects.apiKeys.setApiKeyName(apiKeyName);
|
||||
await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout();
|
||||
},
|
||||
|
||||
async expectNoPermissionsMessage() {
|
||||
await testSubjects.existOrFail('apiKeyFormNoUserPrivileges');
|
||||
},
|
||||
};
|
||||
}
|
|
@ -90,5 +90,11 @@ export function SvlSearchElasticsearchStartPageProvider({ getService }: FtrProvi
|
|||
);
|
||||
expect(await testSubjects.getAttribute('startO11yTrialBtn', 'target')).equal('_blank');
|
||||
},
|
||||
async expectAPIKeyVisibleInCodeBlock(apiKey: string) {
|
||||
await testSubjects.existOrFail('createIndex-code-block');
|
||||
await retry.try(async () => {
|
||||
expect(await testSubjects.getVisibleText('createIndex-code-block')).to.contain(apiKey);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -157,5 +157,12 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont
|
|||
await testSubjects.existOrFail('tryInConsoleButton');
|
||||
await testSubjects.click('tryInConsoleButton');
|
||||
},
|
||||
|
||||
async expectAPIKeyToBeVisibleInCodeBlock(apiKey: string) {
|
||||
await testSubjects.existOrFail('ingestDataCodeExample-code-block');
|
||||
expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain(
|
||||
apiKey
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
import { testHasEmbeddedConsole } from './embedded_console';
|
||||
|
@ -14,10 +15,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
'svlCommonPage',
|
||||
'embeddedConsole',
|
||||
'svlSearchElasticsearchStartPage',
|
||||
'svlApiKeys',
|
||||
]);
|
||||
const svlSearchNavigation = getService('svlSearchNavigation');
|
||||
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
||||
const es = getService('es');
|
||||
const browser = getService('browser');
|
||||
|
||||
const deleteAllTestIndices = async () => {
|
||||
await esDeleteAllIndices(['search-*', 'test-*']);
|
||||
|
@ -27,6 +30,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
describe('developer', function () {
|
||||
before(async () => {
|
||||
await pageObjects.svlCommonPage.loginWithRole('developer');
|
||||
await pageObjects.svlApiKeys.deleteAPIKeys();
|
||||
});
|
||||
after(async () => {
|
||||
await deleteAllTestIndices();
|
||||
|
@ -82,6 +86,58 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexUIView();
|
||||
});
|
||||
|
||||
it('should show the api key in code view', async () => {
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton();
|
||||
await pageObjects.svlApiKeys.expectAPIKeyAvailable();
|
||||
const apiKeyUI = await pageObjects.svlApiKeys.getAPIKeyFromUI();
|
||||
const apiKeySession = await pageObjects.svlApiKeys.getAPIKeyFromSessionStorage();
|
||||
|
||||
expect(apiKeyUI).to.eql(apiKeySession.encoded);
|
||||
|
||||
// check that when browser is refreshed, the api key is still available
|
||||
await browser.refresh();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton();
|
||||
await pageObjects.svlApiKeys.expectAPIKeyAvailable();
|
||||
const refreshBrowserApiKeyUI = await pageObjects.svlApiKeys.getAPIKeyFromUI();
|
||||
expect(refreshBrowserApiKeyUI).to.eql(apiKeyUI);
|
||||
|
||||
// check that when api key is invalidated, a new one is generated
|
||||
await pageObjects.svlApiKeys.invalidateAPIKey(apiKeySession.id);
|
||||
await browser.refresh();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton();
|
||||
await pageObjects.svlApiKeys.expectAPIKeyAvailable();
|
||||
const newApiKeyUI = await pageObjects.svlApiKeys.getAPIKeyFromUI();
|
||||
expect(newApiKeyUI).to.not.eql(apiKeyUI);
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectAPIKeyVisibleInCodeBlock(
|
||||
newApiKeyUI
|
||||
);
|
||||
});
|
||||
|
||||
it('should explicitly ask to create api key when project already has an apikey', async () => {
|
||||
await pageObjects.svlApiKeys.clearAPIKeySessionStorage();
|
||||
await pageObjects.svlApiKeys.createAPIKey();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton();
|
||||
await pageObjects.svlApiKeys.createApiKeyFromFlyout();
|
||||
await pageObjects.svlApiKeys.expectAPIKeyAvailable();
|
||||
});
|
||||
|
||||
it('Same API Key should be present on start page and index detail view', async () => {
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton();
|
||||
await pageObjects.svlApiKeys.expectAPIKeyAvailable();
|
||||
const apiKeyUI = await pageObjects.svlApiKeys.getAPIKeyFromUI();
|
||||
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickUIViewButton();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickCreateIndexButton();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage();
|
||||
|
||||
await pageObjects.svlApiKeys.expectAPIKeyAvailable();
|
||||
const indexDetailsApiKey = await pageObjects.svlApiKeys.getAPIKeyFromUI();
|
||||
|
||||
expect(apiKeyUI).to.eql(indexDetailsApiKey);
|
||||
});
|
||||
|
||||
it('should have file upload link', async () => {
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickFileUploadLink();
|
||||
|
@ -93,32 +149,41 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await pageObjects.svlSearchElasticsearchStartPage.expectAnalyzeLogsLink();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectO11yTrialLink();
|
||||
});
|
||||
});
|
||||
describe('viewer', function () {
|
||||
before(async () => {
|
||||
await pageObjects.svlCommonPage.loginAsViewer();
|
||||
await deleteAllTestIndices();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await svlSearchNavigation.navigateToElasticsearchStartPage();
|
||||
});
|
||||
after(async () => {
|
||||
await deleteAllTestIndices();
|
||||
});
|
||||
|
||||
it('should default to code view when lacking create index permissions', async () => {
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickUIViewButton();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexUIView();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexButtonToBeDisabled();
|
||||
});
|
||||
describe('viewer', function () {
|
||||
before(async () => {
|
||||
await pageObjects.svlCommonPage.loginAsViewer();
|
||||
await deleteAllTestIndices();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await svlSearchNavigation.navigateToElasticsearchStartPage();
|
||||
});
|
||||
after(async () => {
|
||||
await deleteAllTestIndices();
|
||||
});
|
||||
|
||||
it('should redirect to index details when index is created via API', async () => {
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView();
|
||||
await es.indices.create({ index: 'test-my-api-index' });
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage();
|
||||
it('should default to code view when lacking create index permissions', async () => {
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickUIViewButton();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexUIView();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexButtonToBeDisabled();
|
||||
});
|
||||
|
||||
it('should not create an API key if the user only has viewer permissions', async () => {
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton();
|
||||
await pageObjects.svlApiKeys.expectNoPermissionsMessage();
|
||||
const apiKey = await pageObjects.svlApiKeys.getAPIKeyFromSessionStorage();
|
||||
expect(apiKey).to.be(null);
|
||||
});
|
||||
|
||||
it('should redirect to index details when index is created via API', async () => {
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView();
|
||||
await es.indices.create({ index: 'test-my-api-index' });
|
||||
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
'svlCommonPage',
|
||||
'embeddedConsole',
|
||||
'svlSearchIndexDetailPage',
|
||||
'svlApiKeys',
|
||||
]);
|
||||
const svlSearchNavigation = getService('svlSearchNavigation');
|
||||
const es = getService('es');
|
||||
|
@ -22,6 +23,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
describe('Search index detail page', () => {
|
||||
before(async () => {
|
||||
await pageObjects.svlCommonPage.loginWithRole('developer');
|
||||
await pageObjects.svlApiKeys.deleteAPIKeys();
|
||||
});
|
||||
after(async () => {
|
||||
await esDeleteAllIndices(indexName);
|
||||
|
@ -82,6 +84,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await pageObjects.embeddedConsole.clickEmbeddedConsoleControlBar();
|
||||
});
|
||||
|
||||
it('should show api key', async () => {
|
||||
await pageObjects.svlApiKeys.expectAPIKeyAvailable();
|
||||
const apiKey = await pageObjects.svlApiKeys.getAPIKeyFromUI();
|
||||
await pageObjects.svlSearchIndexDetailPage.expectAPIKeyToBeVisibleInCodeBlock(apiKey);
|
||||
});
|
||||
|
||||
it('back to indices button should redirect to list page', async () => {
|
||||
await pageObjects.svlSearchIndexDetailPage.expectBackToIndicesButtonExists();
|
||||
await pageObjects.svlSearchIndexDetailPage.clickBackToIndicesButton();
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -6283,6 +6283,14 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/search-api-keys-components@link:packages/kbn-search-api-keys-components":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/search-api-keys-server@link:packages/kbn-search-api-keys-server":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/search-api-panels@link:packages/kbn-search-api-panels":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -6335,6 +6343,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/search-shared-ui@link:x-pack/packages/search/shared_ui":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/search-types@link:packages/kbn-search-types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue