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 4 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}** app in {kib}. For more information, see
nreese marked this conversation as resolved.
Show resolved Hide resolved
{ref}/security-privileges.html[Security privileges],
<<kibana-privileges, {kib} privileges]>>, and <<xpack-kibana-role-management, {kib} role management>>.
nreese marked this conversation as resolved.
Show resolved Hide resolved

To upload GeoJson files in {kib} with *Maps*, you must have:
nreese marked this conversation as resolved.
Show resolved Hide resolved
* [ ] `all` {kib}
nreese marked this conversation as resolved.
Show resolved Hide resolved
privilege for the `Maps` feature
nreese marked this conversation as resolved.
Show resolved Hide resolved
* [ ] `all` {kib}
privilege for the `Index Pattern Management` feature
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
privilege for the `Index Pattern Management` feature
privilege for the *Index Patterns* feature in *Stack Management*

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Need to keep the word "Index Pattern Management", https://www.elastic.co/guide/en/kibana/current/index-patterns.html#index-patterns-read-only-access. That is the name of the feature priviliage in the UI

Screen Shot 2021-03-31 at 4 03 52 PM

* [ ] `create` and `create_index`index privileges for destination indices.
nreese marked this conversation as resolved.
Show resolved Hide resolved
To use the index in Maps, you must also have `read` and `view_index_metadata` index privileges.
nreese marked this conversation as resolved.
Show resolved Hide resolved

To upload CSV files in {kib} with the *{file-data-viz}*, in addition to upload GeoJson privileges, to you must have:
nreese marked this conversation as resolved.
Show resolved Hide resolved
* [ ] `manage_pipeline` cluster privilege
nreese marked this conversation as resolved.
Show resolved Hide resolved
* [ ] `read` {kib} privilege for the `Machine Learning` feature
nreese marked this conversation as resolved.
Show resolved Hide resolved
* [ ] `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
120 changes: 92 additions & 28 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 @@ -90,14 +93,62 @@ export class ImportCompleteView extends Component<Props, {}> {
}

_getStatusMsg() {
if (this.props.failedPermissionCheck) {
return (
<EuiCallOut
title={i18n.translate('xpack.fileUpload.uploadFailureTitle', {
defaultMessage: 'File upload failed',
nreese marked this conversation as resolved.
Show resolved Hide resolved
})}
color="danger"
iconType="alert"
>
<p>
{i18n.translate('xpack.fileUpload.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.permission.docLink', {
defaultMessage: 'View file import permissions.',
nreese marked this conversation as resolved.
Show resolved Hide resolved
})}
</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.error
? i18n.translate('xpack.fileUpload.uploadFailureMsgErrorBlock', {
defaultMessage: 'Error: {reason}',
values: { reason: this.props.importResults.error.error.reason },
})
: '';
return (
<EuiCallOut
title={i18n.translate('xpack.fileUpload.uploadFailureTitle', {
defaultMessage: 'File upload failed',
nreese marked this conversation as resolved.
Show resolved Hide resolved
})}
color="danger"
iconType="alert"
>
<p>
{i18n.translate('xpack.fileUpload.uploadFailureMsg', {
defaultMessage: 'Unable to upload file. {errorMsg}',
nreese marked this conversation as resolved.
Show resolved Hide resolved
values: { errorMsg },
})}
</p>
</EuiCallOut>
);
}

const successMsg = i18n.translate('xpack.fileUpload.uploadSuccessMsg', {
defaultMessage: 'File upload complete: indexed {numFeatures} features.',
defaultMessage: 'Indexed {numFeatures} features.',
values: {
numFeatures: this.props.importResults.docCount,
},
Expand All @@ -112,15 +163,45 @@ export class ImportCompleteView extends Component<Props, {}> {
})
: '';

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

_renderIndexManagementCallout() {
return this.props.importResults && this.props.importResults.success ? (
<EuiText>
<p>
<FormattedMessage
id="xpack.fileUpload.jsonImport.indexModsMsg"
nreese marked this conversation as resolved.
Show resolved Hide resolved
defaultMessage="Further index modifications can be made using "
nreese marked this conversation as resolved.
Show resolved Hide resolved
/>
<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>
</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', {
Expand All @@ -135,24 +216,7 @@ export class ImportCompleteView extends Component<Props, {}> {
}),
'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._renderIndexManagementCallout()}
</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