mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Adding privilege checks to File Data Visualizer (#29109)
* [ML] Adding privilege checks to File Data Visualizer * fixing typo in comment
This commit is contained in:
parent
a81419cb1d
commit
c91ec0d1ef
8 changed files with 204 additions and 129 deletions
|
@ -18,7 +18,7 @@ import {
|
|||
|
||||
import { MODE as DATAVISUALIZER_MODE } from '../file_datavisualizer_view';
|
||||
|
||||
export function BottomBar({ showBar, mode, changeMode, onCancel }) {
|
||||
export function BottomBar({ showBar, mode, changeMode, onCancel, disableImport }) {
|
||||
if (showBar) {
|
||||
if (mode === DATAVISUALIZER_MODE.READ) {
|
||||
return (
|
||||
|
@ -27,6 +27,7 @@ export function BottomBar({ showBar, mode, changeMode, onCancel }) {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
isDisabled={(disableImport === true)}
|
||||
onClick={() => changeMode(DATAVISUALIZER_MODE.IMPORT)}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -29,7 +29,8 @@ import {
|
|||
createUrlOverrides,
|
||||
processResults,
|
||||
reduceData,
|
||||
} from './utils';
|
||||
hasImportPermission,
|
||||
} from '../utils';
|
||||
|
||||
export const MODE = {
|
||||
READ: 0,
|
||||
|
@ -55,6 +56,7 @@ export class FileDataVisualizerView extends Component {
|
|||
mode: MODE.READ,
|
||||
isEditFlyoutVisible: false,
|
||||
bottomBarVisible: false,
|
||||
hasPermissionToImport: false,
|
||||
};
|
||||
|
||||
this.overrides = {};
|
||||
|
@ -62,6 +64,14 @@ export class FileDataVisualizerView extends Component {
|
|||
this.originalSettings = {};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// check the user has the correct permission to import data.
|
||||
// note, calling hasImportPermission with no arguments just checks the
|
||||
// cluster privileges, the user will still need index privileges to create and ingest
|
||||
const hasPermissionToImport = await hasImportPermission();
|
||||
this.setState({ hasPermissionToImport });
|
||||
}
|
||||
|
||||
onFilePickerChange = (files) => {
|
||||
this.overrides = {};
|
||||
|
||||
|
@ -242,6 +252,7 @@ export class FileDataVisualizerView extends Component {
|
|||
mode,
|
||||
isEditFlyoutVisible,
|
||||
bottomBarVisible,
|
||||
hasPermissionToImport,
|
||||
} = this.state;
|
||||
|
||||
const fields = (results !== undefined && results.field_stats !== undefined) ? Object.keys(results.field_stats) : [];
|
||||
|
@ -302,6 +313,7 @@ export class FileDataVisualizerView extends Component {
|
|||
mode={MODE.READ}
|
||||
changeMode={this.changeMode}
|
||||
onCancel={this.onCancel}
|
||||
disableImport={(hasPermissionToImport === false)}
|
||||
/>
|
||||
|
||||
<BottomPadding />
|
||||
|
|
|
@ -69,6 +69,13 @@ function title(statuses) {
|
|||
defaultMessage="Error creating index pattern"
|
||||
/>
|
||||
);
|
||||
case statuses.permissionCheckStatus:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.fileDatavisualizer.importErrors.checkingPermissionErrorMessage"
|
||||
defaultMessage="Import permissions error"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<FormattedMessage
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { importerFactory } from './importer';
|
||||
import { ResultsLinks } from '../results_links';
|
||||
import { ImportProgress, IMPORT_STATUS } from '../import_progress';
|
||||
|
@ -26,6 +27,7 @@ import { ImportSettings } from '../import_settings';
|
|||
import { ExperimentalBadge } from '../experimental_badge';
|
||||
import { getIndexPatternNames, refreshIndexPatterns } from '../../../util/index_utils';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { hasImportPermission } from '../utils';
|
||||
|
||||
const DEFAULT_TIME_FIELD = '@timestamp';
|
||||
const CONFIG_MODE = { SIMPLE: 0, ADVANCED: 1 };
|
||||
|
@ -41,6 +43,7 @@ const DEFAULT_STATE = {
|
|||
indexCreatedStatus: IMPORT_STATUS.INCOMPLETE,
|
||||
indexPatternCreatedStatus: IMPORT_STATUS.INCOMPLETE,
|
||||
ingestPipelineCreatedStatus: IMPORT_STATUS.INCOMPLETE,
|
||||
permissionCheckStatus: IMPORT_STATUS.INCOMPLETE,
|
||||
uploadProgress: 0,
|
||||
uploadStatus: IMPORT_STATUS.INCOMPLETE,
|
||||
createIndexPattern: true,
|
||||
|
@ -111,128 +114,151 @@ export class ImportView extends Component {
|
|||
if (index !== '') {
|
||||
this.setState({
|
||||
importing: true,
|
||||
imported: false,
|
||||
reading: true,
|
||||
initialized: true,
|
||||
}, () => {
|
||||
this.props.hideBottomBar();
|
||||
setTimeout(async () => {
|
||||
let success = false;
|
||||
const createPipeline = (pipelineString !== '');
|
||||
|
||||
let indexCreationSettings = {};
|
||||
try {
|
||||
const settings = JSON.parse(indexSettingsString);
|
||||
const mappings = JSON.parse(mappingsString);
|
||||
indexCreationSettings = {
|
||||
settings,
|
||||
mappings,
|
||||
};
|
||||
if (createPipeline) {
|
||||
indexCreationSettings.pipeline = JSON.parse(pipelineString);
|
||||
errors,
|
||||
}, async () => {
|
||||
// check to see if the user has permission to create and ingest data into the specified index
|
||||
if (await hasImportPermission(index) === false) {
|
||||
errors.push(i18n.translate('xpack.ml.fileDatavisualizer.importView.importPermissionError', {
|
||||
defaultMessage: 'You do not have permission to create or import data into index {index}.',
|
||||
values: {
|
||||
index
|
||||
}
|
||||
}));
|
||||
this.setState({
|
||||
permissionCheckStatus: IMPORT_STATUS.FAILED,
|
||||
importing: false,
|
||||
imported: false,
|
||||
errors
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// if an @timestamp field has been added to the
|
||||
// mappings, use this field as the time field.
|
||||
// This relies on the field being populated by
|
||||
// the ingest pipeline on ingest
|
||||
if (mappings[DEFAULT_TIME_FIELD] !== undefined) {
|
||||
timeFieldName = DEFAULT_TIME_FIELD;
|
||||
this.setState({ timeFieldName });
|
||||
}
|
||||
this.setState({
|
||||
importing: true,
|
||||
imported: false,
|
||||
reading: true,
|
||||
initialized: true,
|
||||
permissionCheckStatus: IMPORT_STATUS.COMPLETE,
|
||||
}, () => {
|
||||
this.props.hideBottomBar();
|
||||
setTimeout(async () => {
|
||||
let success = false;
|
||||
const createPipeline = (pipelineString !== '');
|
||||
|
||||
success = true;
|
||||
} catch (error) {
|
||||
success = false;
|
||||
errors.push(error);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
const importer = importerFactory(format, results, indexCreationSettings);
|
||||
if (importer !== undefined) {
|
||||
|
||||
const readResp = await importer.read(fileContents, this.setReadProgress);
|
||||
success = readResp.success;
|
||||
this.setState({
|
||||
readStatus: success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
|
||||
reading: false,
|
||||
});
|
||||
|
||||
if (readResp.success === false) {
|
||||
console.error(readResp.error);
|
||||
errors.push(readResp.error);
|
||||
let indexCreationSettings = {};
|
||||
try {
|
||||
const settings = JSON.parse(indexSettingsString);
|
||||
const mappings = JSON.parse(mappingsString);
|
||||
indexCreationSettings = {
|
||||
settings,
|
||||
mappings,
|
||||
};
|
||||
if (createPipeline) {
|
||||
indexCreationSettings.pipeline = JSON.parse(pipelineString);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
const initializeImportResp = await importer.initializeImport(index);
|
||||
// if an @timestamp field has been added to the
|
||||
// mappings, use this field as the time field.
|
||||
// This relies on the field being populated by
|
||||
// the ingest pipeline on ingest
|
||||
if (mappings[DEFAULT_TIME_FIELD] !== undefined) {
|
||||
timeFieldName = DEFAULT_TIME_FIELD;
|
||||
this.setState({ timeFieldName });
|
||||
}
|
||||
|
||||
const indexCreated = (initializeImportResp.index !== undefined);
|
||||
success = true;
|
||||
} catch (error) {
|
||||
success = false;
|
||||
errors.push(error);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
const importer = importerFactory(format, results, indexCreationSettings);
|
||||
if (importer !== undefined) {
|
||||
|
||||
const readResp = await importer.read(fileContents, this.setReadProgress);
|
||||
success = readResp.success;
|
||||
this.setState({
|
||||
indexCreatedStatus: indexCreated ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
|
||||
readStatus: success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
|
||||
reading: false,
|
||||
});
|
||||
|
||||
if (createPipeline) {
|
||||
const pipelineCreated = (initializeImportResp.pipelineId !== undefined);
|
||||
if (indexCreated) {
|
||||
this.setState({
|
||||
ingestPipelineCreatedStatus: pipelineCreated ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
|
||||
ingestPipelineId: pipelineCreated ? initializeImportResp.pipelineId : '',
|
||||
});
|
||||
}
|
||||
success = (indexCreated && pipelineCreated);
|
||||
} else {
|
||||
success = indexCreated;
|
||||
if (readResp.success === false) {
|
||||
console.error(readResp.error);
|
||||
errors.push(readResp.error);
|
||||
}
|
||||
|
||||
|
||||
if (success) {
|
||||
const importId = initializeImportResp.id;
|
||||
const pipelineId = initializeImportResp.pipelineId;
|
||||
const importResp = await importer.import(importId, index, pipelineId, this.setImportProgress);
|
||||
success = importResp.success;
|
||||
const initializeImportResp = await importer.initializeImport(index);
|
||||
|
||||
const indexCreated = (initializeImportResp.index !== undefined);
|
||||
this.setState({
|
||||
uploadStatus: importResp.success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
|
||||
importFailures: importResp.failures,
|
||||
docCount: importResp.docCount,
|
||||
indexCreatedStatus: indexCreated ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
if (createIndexPattern) {
|
||||
const indexPatternName = (indexPattern === '') ? index : indexPattern;
|
||||
const indexPatternResp = await createKibanaIndexPattern(
|
||||
indexPatternName,
|
||||
indexPatterns,
|
||||
timeFieldName,
|
||||
kibanaConfig,
|
||||
);
|
||||
success = indexPatternResp.success;
|
||||
if (createPipeline) {
|
||||
const pipelineCreated = (initializeImportResp.pipelineId !== undefined);
|
||||
if (indexCreated) {
|
||||
this.setState({
|
||||
indexPatternCreatedStatus: indexPatternResp.success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
|
||||
indexPatternId: indexPatternResp.id,
|
||||
ingestPipelineCreatedStatus: pipelineCreated ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
|
||||
ingestPipelineId: pipelineCreated ? initializeImportResp.pipelineId : '',
|
||||
});
|
||||
if (indexPatternResp.success === false) {
|
||||
errors.push(indexPatternResp.error);
|
||||
}
|
||||
success = (indexCreated && pipelineCreated);
|
||||
} else {
|
||||
success = indexCreated;
|
||||
}
|
||||
|
||||
|
||||
if (success) {
|
||||
const importId = initializeImportResp.id;
|
||||
const pipelineId = initializeImportResp.pipelineId;
|
||||
const importResp = await importer.import(importId, index, pipelineId, this.setImportProgress);
|
||||
success = importResp.success;
|
||||
this.setState({
|
||||
uploadStatus: importResp.success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
|
||||
importFailures: importResp.failures,
|
||||
docCount: importResp.docCount,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
if (createIndexPattern) {
|
||||
const indexPatternName = (indexPattern === '') ? index : indexPattern;
|
||||
const indexPatternResp = await createKibanaIndexPattern(
|
||||
indexPatternName,
|
||||
indexPatterns,
|
||||
timeFieldName,
|
||||
kibanaConfig,
|
||||
);
|
||||
success = indexPatternResp.success;
|
||||
this.setState({
|
||||
indexPatternCreatedStatus: indexPatternResp.success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
|
||||
indexPatternId: indexPatternResp.id,
|
||||
});
|
||||
if (indexPatternResp.success === false) {
|
||||
errors.push(indexPatternResp.error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push(importResp.error);
|
||||
}
|
||||
} else {
|
||||
errors.push(importResp.error);
|
||||
errors.push(initializeImportResp.error);
|
||||
}
|
||||
} else {
|
||||
errors.push(initializeImportResp.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showBottomBar();
|
||||
showBottomBar();
|
||||
|
||||
this.setState({
|
||||
importing: false,
|
||||
imported: success,
|
||||
errors,
|
||||
});
|
||||
this.setState({
|
||||
importing: false,
|
||||
imported: success,
|
||||
errors,
|
||||
});
|
||||
|
||||
}, 500);
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -322,6 +348,7 @@ export class ImportView extends Component {
|
|||
indexCreatedStatus,
|
||||
ingestPipelineCreatedStatus,
|
||||
indexPatternCreatedStatus,
|
||||
permissionCheckStatus,
|
||||
uploadProgress,
|
||||
uploadStatus,
|
||||
createIndexPattern,
|
||||
|
@ -344,6 +371,7 @@ export class ImportView extends Component {
|
|||
indexCreatedStatus,
|
||||
ingestPipelineCreatedStatus,
|
||||
indexPatternCreatedStatus,
|
||||
permissionCheckStatus,
|
||||
uploadProgress,
|
||||
uploadStatus,
|
||||
createIndexPattern,
|
||||
|
@ -466,21 +494,20 @@ export class ImportView extends Component {
|
|||
|
||||
</EuiPanel>
|
||||
|
||||
{
|
||||
(errors.length > 0) &&
|
||||
<React.Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<ImportErrors
|
||||
errors={errors}
|
||||
statuses={statuses}
|
||||
/>
|
||||
|
||||
</React.Fragment>
|
||||
}
|
||||
</React.Fragment>
|
||||
}
|
||||
{
|
||||
(errors.length > 0) &&
|
||||
<React.Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<ImportErrors
|
||||
errors={errors}
|
||||
statuses={statuses}
|
||||
/>
|
||||
|
||||
</React.Fragment>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
export * from './utils';
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { overrideDefaults } from './overrides';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
|
||||
export function readFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -105,3 +106,30 @@ export function processResults(results) {
|
|||
grokPattern: results.grok_pattern,
|
||||
};
|
||||
}
|
||||
|
||||
// a check for the minimum privileges needed to create and ingest data into an index.
|
||||
// if called with no indexName, the check will just look for the minimum cluster privileges.
|
||||
export async function hasImportPermission(indexName) {
|
||||
const priv = {
|
||||
cluster: [
|
||||
'cluster:monitor/nodes/info',
|
||||
'cluster:admin/ingest/pipeline/put',
|
||||
]
|
||||
};
|
||||
|
||||
if (indexName !== undefined) {
|
||||
priv.index = [
|
||||
{
|
||||
names: [indexName],
|
||||
privileges: [
|
||||
'indices:data/write/bulk',
|
||||
'indices:data/write/index',
|
||||
'indices:admin/create',
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const resp = await ml.checkPrivilege(priv);
|
||||
return (resp.securityDisabled === true || resp.has_all_requested === true);
|
||||
}
|
|
@ -41,7 +41,7 @@ export function importDataProvider(callWithRequest) {
|
|||
}
|
||||
|
||||
let failures = [];
|
||||
if (data.length && indexExits(index)) {
|
||||
if (data.length) {
|
||||
const resp = await indexData(index, createdPipelineId, data);
|
||||
if (resp.success === false) {
|
||||
if (resp.ingestError) {
|
||||
|
@ -78,24 +78,20 @@ export function importDataProvider(callWithRequest) {
|
|||
}
|
||||
|
||||
async function createIndex(index, settings, mappings) {
|
||||
if (await indexExits(index) === false) {
|
||||
const body = {
|
||||
mappings: {
|
||||
_meta: {
|
||||
created_by: INDEX_META_DATA_CREATED_BY
|
||||
},
|
||||
properties: mappings
|
||||
}
|
||||
};
|
||||
|
||||
if (settings && Object.keys(settings).length) {
|
||||
body.settings = settings;
|
||||
const body = {
|
||||
mappings: {
|
||||
_meta: {
|
||||
created_by: INDEX_META_DATA_CREATED_BY
|
||||
},
|
||||
properties: mappings
|
||||
}
|
||||
};
|
||||
|
||||
await callWithRequest('indices.create', { index, body });
|
||||
} else {
|
||||
throw `${index} already exists.`;
|
||||
if (settings && Object.keys(settings).length) {
|
||||
body.settings = settings;
|
||||
}
|
||||
|
||||
await callWithRequest('indices.create', { index, body });
|
||||
}
|
||||
|
||||
async function indexData(index, pipelineId, data) {
|
||||
|
@ -145,10 +141,6 @@ export function importDataProvider(callWithRequest) {
|
|||
|
||||
}
|
||||
|
||||
async function indexExits(index) {
|
||||
return await callWithRequest('indices.exists', { index });
|
||||
}
|
||||
|
||||
async function createPipeline(id, pipeline) {
|
||||
return await callWithRequest('ingest.putPipeline', { id, body: pipeline });
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue