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

[file upload] document file upload privileges and provide actionable UI when failures occur #95883

Merged
merged 18 commits into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from 14 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
24 changes: 24 additions & 0 deletions docs/maps/import-geospatial-data.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ To import geospatical data into the Elastic Stack, the data must be indexed as {
Geospatial data comes in many formats.
Choose an import tool based on the format of your geospatial data.

[discrete]
[[import-geospatial-privileges]]
=== Security privileges

The {stack-security-features} provide roles and privileges that control which users can upload files.
You can manage your roles, privileges, and
spaces in the **{stack-manage-app}** in {kib}. For more information, see
{ref}/security-privileges.html[Security privileges],
<<kibana-privileges, {kib} privileges>>, and <<xpack-kibana-role-management, {kib} role management>>.

To upload GeoJSON files in {kib} with *Maps*, you must have:

* `all` {kib} privilege for `Maps`
* `all` {kib} privilege for `Index Pattern Management`
* `create` and `create_index` index privileges for destination indices.
* To use the index in Maps, you must also have `read` and `view_index_metadata` index privileges for destination indices.

To upload CSV files in {kib} with the *{file-data-viz}*, you must have privileges to upload GeoJSON files and:

* `manage_pipeline` cluster privilege
* `read` {kib} privilege for `Machine Learning`
* `machine_learning_admin` or `machine_learning_user` role


[discrete]
=== Upload CSV with latitude and longitude columns

Expand Down
1 change: 1 addition & 0 deletions src/core/public/doc_links/doc_links_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export class DocLinksService {
},
maps: {
guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`,
importGeospatialPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/import-geospatial-data.html#import-geospatial-privileges`,
},
monitoring: {
alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`,
Expand Down
5 changes: 4 additions & 1 deletion x-pack/plugins/file_upload/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import type { estypes } from '@elastic/elasticsearch';
import { ES_FIELD_TYPES } from '../../../../src/plugins/data/common';

export interface HasImportPermission {
Expand Down Expand Up @@ -83,7 +84,9 @@ export interface ImportResponse {
pipelineId?: string;
docCount: number;
failures: ImportFailure[];
error?: any;
error?: {
error: estypes.ErrorCause;
};
ingestError?: boolean;
}

Expand Down
138 changes: 103 additions & 35 deletions x-pack/plugins/file_upload/public/components/import_complete_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,31 @@

import React, { Component, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButtonIcon,
EuiCallOut,
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { CodeEditor, KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import { getHttp, getUiSettings } from '../kibana_services';
import { getDocLinks, getHttp, getUiSettings } from '../kibana_services';
import { ImportResults } from '../importer';

const services = {
uiSettings: getUiSettings(),
};

interface Props {
failedPermissionCheck: boolean;
importResults?: ImportResults;
indexPatternResp?: object;
indexName: string;
}

export class ImportCompleteView extends Component<Props, {}> {
Expand Down Expand Up @@ -57,9 +60,12 @@ export class ImportCompleteView extends Component<Props, {}> {
iconType="copy"
color="text"
data-test-subj={copyButtonDataTestSubj}
aria-label={i18n.translate('xpack.fileUpload.copyButtonAriaLabel', {
defaultMessage: 'Copy to clipboard',
})}
aria-label={i18n.translate(
'xpack.fileUpload.importComplete.copyButtonAriaLabel',
{
defaultMessage: 'Copy to clipboard',
}
)}
/>
)}
</EuiCopy>
Expand Down Expand Up @@ -90,69 +96,131 @@ export class ImportCompleteView extends Component<Props, {}> {
}

_getStatusMsg() {
if (this.props.failedPermissionCheck) {
return (
<EuiCallOut
title={i18n.translate('xpack.fileUpload.importComplete.uploadFailureTitle', {
defaultMessage: 'Unable to upload file',
})}
color="danger"
iconType="alert"
>
<p>
{i18n.translate('xpack.fileUpload.importComplete.permissionFailureMsg', {
defaultMessage:
'You do not have permission to create or import data into index "{indexName}".',
values: { indexName: this.props.indexName },
})}
</p>
<EuiLink
href={getDocLinks().links.maps.importGeospatialPrivileges}
target="_blank"
external
>
{i18n.translate('xpack.fileUpload.importComplete.permission.docLink', {
defaultMessage: 'View file import permissions',
})}
</EuiLink>
</EuiCallOut>
);
}

if (!this.props.importResults || !this.props.importResults.success) {
return i18n.translate('xpack.fileUpload.uploadFailureMsg', {
defaultMessage: 'File upload failed.',
});
const errorMsg =
this.props.importResults && this.props.importResults.error
? i18n.translate('xpack.fileUpload.importComplete.uploadFailureMsgErrorBlock', {
defaultMessage: 'Error: {reason}',
values: { reason: this.props.importResults.error.error.reason },
})
: '';
return (
<EuiCallOut
title={i18n.translate('xpack.fileUpload.importComplete.uploadFailureTitle', {
defaultMessage: 'Unable to upload file',
})}
color="danger"
iconType="alert"
>
<p>
{i18n.translate('xpack.fileUpload.importComplete.uploadFailureMsg', {
defaultMessage: 'Unable to upload file. {errorMsg}',
values: { errorMsg },
})}
</p>
</EuiCallOut>
);
}

const successMsg = i18n.translate('xpack.fileUpload.uploadSuccessMsg', {
defaultMessage: 'File upload complete: indexed {numFeatures} features.',
const successMsg = i18n.translate('xpack.fileUpload.importComplete.uploadSuccessMsg', {
defaultMessage: 'Indexed {numFeatures} features.',
values: {
numFeatures: this.props.importResults.docCount,
},
});

const failedFeaturesMsg = this.props.importResults.failures?.length
? i18n.translate('xpack.fileUpload.failedFeaturesMsg', {
? i18n.translate('xpack.fileUpload.importComplete.failedFeaturesMsg', {
defaultMessage: 'Unable to index {numFailures} features.',
values: {
numFailures: this.props.importResults.failures.length,
},
})
: '';

return `${successMsg} ${failedFeaturesMsg}`;
return (
<EuiCallOut
title={i18n.translate('xpack.fileUpload.importComplete.uploadSuccessTitle', {
defaultMessage: 'File upload complete',
})}
>
<p>{`${successMsg} ${failedFeaturesMsg}`}</p>
</EuiCallOut>
);
}

_renderIndexManagementMsg() {
return this.props.importResults && this.props.importResults.success ? (
<EuiText>
<p>
<FormattedMessage
id="xpack.fileUpload.importComplete.indexModsMsg"
defaultMessage="To modify the index, go to "
/>
<a
data-test-subj="indexManagementNewIndexLink"
target="_blank"
href={getHttp().basePath.prepend('/app/management/kibana/indexPatterns')}
>
<FormattedMessage
id="xpack.fileUpload.importComplete.indexMgmtLink"
defaultMessage="Index Management."
/>
</a>
</p>
</EuiText>
) : null;
}

render() {
return (
<KibanaContextProvider services={services}>
<EuiText>
<p>{this._getStatusMsg()}</p>
</EuiText>
{this._getStatusMsg()}

{this._renderCodeEditor(
this.props.importResults,
i18n.translate('xpack.fileUpload.jsonImport.indexingResponse', {
i18n.translate('xpack.fileUpload.importComplete.indexingResponse', {
defaultMessage: 'Import response',
}),
'indexRespCopyButton'
)}
{this._renderCodeEditor(
this.props.indexPatternResp,
i18n.translate('xpack.fileUpload.jsonImport.indexPatternResponse', {
i18n.translate('xpack.fileUpload.importComplete.indexPatternResponse', {
defaultMessage: 'Index pattern response',
}),
'indexPatternRespCopyButton'
)}
<EuiCallOut>
<div>
<FormattedMessage
id="xpack.fileUpload.jsonImport.indexModsMsg"
defaultMessage="Further index modifications can be made using "
/>
<a
data-test-subj="indexManagementNewIndexLink"
target="_blank"
href={getHttp().basePath.prepend('/app/management/kibana/indexPatterns')}
>
<FormattedMessage
id="xpack.fileUpload.jsonImport.indexMgmtLink"
defaultMessage="Index Management"
/>
</a>
</div>
</EuiCallOut>
{this._renderIndexManagementMsg()}
</KibanaContextProvider>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { FileUploadComponentProps } from '../lazy_load_bundle';
import { ImportResults } from '../importer';
import { GeoJsonImporter } from '../importer/geojson_importer';
import { Settings } from '../../common';
import { hasImportPermission } from '../api';

enum PHASE {
CONFIGURE = 'CONFIGURE',
Expand All @@ -31,6 +32,7 @@ function getWritingToIndexMsg(progress: number) {
}

interface State {
failedPermissionCheck: boolean;
geoFieldType: ES_FIELD_TYPES.GEO_POINT | ES_FIELD_TYPES.GEO_SHAPE;
importStatus: string;
importResults?: ImportResults;
Expand All @@ -45,6 +47,7 @@ export class JsonUploadAndParse extends Component<FileUploadComponentProps, Stat
private _isMounted = false;

state: State = {
failedPermissionCheck: false,
geoFieldType: ES_FIELD_TYPES.GEO_SHAPE,
importStatus: '',
indexName: '',
Expand Down Expand Up @@ -74,6 +77,26 @@ export class JsonUploadAndParse extends Component<FileUploadComponentProps, Stat
return;
}

//
// check permissions
//
const canImport = await hasImportPermission({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we short-circuit this if this.state.indexName isn't defined? Looks like it will still do the endpoint roundtrip even if it's an empty string

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button triggering import is only enabled when indexName is set so no need to check for empty string. Logic ensuring this is in _onIndexNameChange

checkCreateIndexPattern: true,
checkHasManagePipeline: false,
indexName: this.state.indexName,
});
if (!this._isMounted) {
return;
}
if (!canImport) {
this.setState({
phase: PHASE.COMPLETE,
failedPermissionCheck: true,
});
this.props.onIndexingError();
return;
}

//
// create index
//
Expand Down Expand Up @@ -111,6 +134,7 @@ export class JsonUploadAndParse extends Component<FileUploadComponentProps, Stat
if (initializeImportResp.index === undefined || initializeImportResp.id === undefined) {
this.setState({
phase: PHASE.COMPLETE,
importResults: initializeImportResp,
});
this.props.onIndexingError();
return;
Expand Down Expand Up @@ -255,6 +279,8 @@ export class JsonUploadAndParse extends Component<FileUploadComponentProps, Stat
<ImportCompleteView
importResults={this.state.importResults}
indexPatternResp={this.state.indexPatternResp}
indexName={this.state.indexName}
failedPermissionCheck={this.state.failedPermissionCheck}
/>
);
}
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/file_upload/public/kibana_services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function setStartServices(core: CoreStart, plugins: FileUploadStartDepend
pluginsStart = plugins;
}

export const getDocLinks = () => coreStart.docLinks;
export const getIndexPatternService = () => pluginsStart.data.indexPatterns;
export const getHttp = () => coreStart.http;
export const getSavedObjectsClient = () => coreStart.savedObjects.client;
Expand Down
4 changes: 0 additions & 4 deletions x-pack/plugins/translations/translations/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -8170,10 +8170,6 @@
"xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage": "インデックス名またはパターンはすでに存在します。",
"xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage": "インデックス名に許可されていない文字が含まれています。",
"xpack.fileUpload.indexSettings.indexNameGuidelines": "インデックス名ガイドライン",
"xpack.fileUpload.jsonImport.indexingResponse": "インデックス応答",
"xpack.fileUpload.jsonImport.indexMgmtLink": "インデックス管理",
"xpack.fileUpload.jsonImport.indexModsMsg": "次を使用すると、その他のインデックス修正を行うことができます。\n",
"xpack.fileUpload.jsonImport.indexPatternResponse": "インデックスパターン応答",
"xpack.fileUpload.jsonUploadAndParse.dataIndexingError": "データインデックスエラー",
"xpack.fileUpload.jsonUploadAndParse.indexPatternError": "インデックスパターンエラー",
"xpack.fleet.agentBulkActions.clearSelection": "選択した項目をクリア",
Expand Down
4 changes: 0 additions & 4 deletions x-pack/plugins/translations/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -8243,10 +8243,6 @@
"xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage": "索引名称或模式已存在。",
"xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage": "索引名称包含非法字符。",
"xpack.fileUpload.indexSettings.indexNameGuidelines": "索引名称指引",
"xpack.fileUpload.jsonImport.indexingResponse": "索引响应",
"xpack.fileUpload.jsonImport.indexMgmtLink": "索引管理",
"xpack.fileUpload.jsonImport.indexModsMsg": "要进一步做索引修改,可以使用\n",
"xpack.fileUpload.jsonImport.indexPatternResponse": "索引模式响应",
"xpack.fileUpload.jsonUploadAndParse.dataIndexingError": "数据索引错误",
"xpack.fileUpload.jsonUploadAndParse.indexPatternError": "索引模式错误",
"xpack.fleet.agentBulkActions.agentsSelected": "已选择 {count, plural, other {# 个代理}}",
Expand Down