Skip to content

Commit

Permalink
[IM] prevent users from editing and deleting cloud-managed templates (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
alisonelizabeth authored and cjcenizal committed Aug 28, 2019
1 parent 1efe813 commit a6335fd
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ describe.skip('<TemplateCreate />', () => {
settings: JSON.stringify(SETTINGS),
mappings: JSON.stringify(MAPPINGS),
aliases: JSON.stringify(ALIASES),
isManaged: false,
})
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ const stringifyJson = (json: any) => {
return JSON.stringify(json, null, 2);
};

export function deserializeTemplateList(indexTemplatesByName: any): TemplateListItem[] {
export function deserializeTemplateList(
indexTemplatesByName: any,
managedTemplatePrefix?: string
): TemplateListItem[] {
const indexTemplateNames: string[] = Object.keys(indexTemplatesByName);

const deserializedTemplates: TemplateListItem[] = indexTemplateNames.map((name: string) => {
Expand All @@ -51,6 +54,7 @@ export function deserializeTemplateList(indexTemplatesByName: any): TemplateList
hasAliases: hasEntries(aliases),
hasMappings: hasEntries(mappings),
ilmPolicy: settings && settings.index && settings.index.lifecycle,
isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)),
};
});

Expand All @@ -73,7 +77,10 @@ export function serializeTemplate(template: Template): TemplateEs {
return serializedTemplate;
}

export function deserializeTemplate(templateEs: TemplateEs): Template {
export function deserializeTemplate(
templateEs: TemplateEs,
managedTemplatePrefix?: string
): Template {
const {
name,
version,
Expand All @@ -93,6 +100,7 @@ export function deserializeTemplate(templateEs: TemplateEs): Template {
aliases: hasEntries(aliases) ? stringifyJson(aliases) : undefined,
mappings: hasEntries(mappings) ? stringifyJson(mappings) : undefined,
ilmPolicy: settings && settings.index && settings.index.lifecycle,
isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)),
};

return deserializedTemplate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface TemplateListItem {
ilmPolicy?: {
name: string;
};
isManaged: boolean;
}
export interface Template {
name: string;
Expand All @@ -27,6 +28,7 @@ export interface Template {
ilmPolicy?: {
name: string;
};
isManaged: boolean;
}

export interface TemplateEs {
Expand Down
2 changes: 1 addition & 1 deletion x-pack/legacy/plugins/index_management/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function indexManagement(kibana) {
server.expose('addIndexManagementDataEnricher', addIndexManagementDataEnricher);
registerLicenseChecker(server, PLUGIN.ID, PLUGIN.NAME, PLUGIN.MINIMUM_LICENSE_REQUIRED);
registerIndicesRoutes(router);
registerTemplateRoutes(router);
registerTemplateRoutes(router, server);
registerSettingsRoutes(router);
registerStatsRoute(router);
registerMappingRoute(router);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, { Fragment, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiCallOut,
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
Expand Down Expand Up @@ -101,34 +102,12 @@ export const TemplateDetails: React.FunctionComponent<Props> = ({
}) => {
const decodedTemplateName = decodePath(templateName);
const { error, data: templateDetails, isLoading } = loadIndexTemplate(decodedTemplateName);
// TS complains if we use destructuring here. Fixed in 3.6.0 (https://github.com/microsoft/TypeScript/pull/31711).
const isManaged = templateDetails ? templateDetails.isManaged : undefined;
const [templateToDelete, setTemplateToDelete] = useState<Array<Template['name']>>([]);
const [activeTab, setActiveTab] = useState<string>(SUMMARY_TAB_ID);
const [isPopoverOpen, setIsPopOverOpen] = useState<boolean>(false);

const contextMenuItems = [
{
name: i18n.translate('xpack.idxMgmt.templateDetails.editButtonLabel', {
defaultMessage: 'Edit',
}),
icon: 'pencil',
onClick: () => editTemplate(decodedTemplateName),
},
{
name: i18n.translate('xpack.idxMgmt.templateDetails.cloneButtonLabel', {
defaultMessage: 'Clone',
}),
icon: 'copy',
onClick: () => cloneTemplate(decodedTemplateName),
},
{
name: i18n.translate('xpack.idxMgmt.templateDetails.deleteButtonLabel', {
defaultMessage: 'Delete',
}),
icon: 'trash',
onClick: () => setTemplateToDelete([decodedTemplateName]),
},
];

let content;

if (isLoading) {
Expand All @@ -155,9 +134,31 @@ export const TemplateDetails: React.FunctionComponent<Props> = ({
);
} else if (templateDetails) {
const Content = tabToComponentMap[activeTab];
const managedTemplateCallout = isManaged ? (
<Fragment>
<EuiCallOut
title={
<FormattedMessage
id="xpack.idxMgmt.templateDetails.managedTemplateInfoTitle"
defaultMessage="Editing a managed template is not permitted"
/>
}
color="primary"
size="s"
>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.managedTemplateInfoDescription"
defaultMessage="Managed templates are critical for internal operations."
/>
</EuiCallOut>
<EuiSpacer size="m" />
</Fragment>
) : null;

content = (
<Fragment>
{managedTemplateCallout}

<EuiTabs>
{TABS.map(tab => (
<EuiTab
Expand Down Expand Up @@ -229,7 +230,6 @@ export const TemplateDetails: React.FunctionComponent<Props> = ({
/>
</EuiButtonEmpty>
</EuiFlexItem>

{templateDetails && (
<EuiFlexItem grow={false}>
{/* Manage templates context menu */}
Expand Down Expand Up @@ -267,7 +267,34 @@ export const TemplateDetails: React.FunctionComponent<Props> = ({
defaultMessage: 'Template options',
}
),
items: contextMenuItems,
items: [
{
name: i18n.translate('xpack.idxMgmt.templateDetails.editButtonLabel', {
defaultMessage: 'Edit',
}),
icon: 'pencil',
onClick: () => editTemplate(decodedTemplateName),
disabled: isManaged,
},
{
name: i18n.translate('xpack.idxMgmt.templateDetails.cloneButtonLabel', {
defaultMessage: 'Clone',
}),
icon: 'copy',
onClick: () => cloneTemplate(decodedTemplateName),
},
{
name: i18n.translate(
'xpack.idxMgmt.templateDetails.deleteButtonLabel',
{
defaultMessage: 'Delete',
}
),
icon: 'trash',
onClick: () => setTemplateToDelete([decodedTemplateName]),
disabled: isManaged,
},
],
},
]}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
onClick: ({ name }: Template) => {
editTemplate(name);
},
enabled: ({ isManaged }: Template) => !isManaged,
},
{
name: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneTitle', {
Expand Down Expand Up @@ -161,6 +162,7 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
setTemplatesToDelete([name]);
},
isPrimary: true,
enabled: ({ isManaged }: Template) => !isManaged,
},
],
},
Expand All @@ -180,6 +182,14 @@ export const TemplateTable: React.FunctionComponent<Props> = ({

const selectionConfig = {
onSelectionChange: setSelection,
selectable: ({ isManaged }: Template) => !isManaged,
selectableMessage: (selectable: boolean) => {
if (!selectable) {
return i18n.translate('xpack.idxMgmt.templateList.table.deleteManagedTemplateTooltip', {
defaultMessage: 'You cannot delete a managed template.',
});
}
},
};

const searchConfig = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const DEFAULT_TEMPLATE: Template = {
settings: emptyObject,
mappings: emptyObject,
aliases: emptyObject,
isManaged: false,
};

export const TemplateCreate: React.FunctionComponent<RouteComponentProps> = ({ history }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,42 +78,63 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara
/>
);
} else if (template) {
const { name: templateName } = template;
const { name: templateName, isManaged } = template;
const isSystemTemplate = templateName && templateName.startsWith('.');

content = (
<Fragment>
{isSystemTemplate && (
<Fragment>
<EuiCallOut
title={
if (isManaged) {
content = (
<EuiCallOut
title={
<FormattedMessage
id="xpack.idxMgmt.templateEdit.managedTemplateWarningTitle"
defaultMessage="Editing a managed template is not permitted"
/>
}
color="danger"
iconType="alert"
data-test-subj="systemTemplateEditCallout"
>
<FormattedMessage
id="xpack.idxMgmt.templateEdit.managedTemplateWarningDescription"
defaultMessage="Managed templates are critical for internal operations."
/>
</EuiCallOut>
);
} else {
content = (
<Fragment>
{isSystemTemplate && (
<Fragment>
<EuiCallOut
title={
<FormattedMessage
id="xpack.idxMgmt.templateEdit.systemTemplateWarningTitle"
defaultMessage="Editing a system template can break Kibana"
/>
}
color="danger"
iconType="alert"
data-test-subj="systemTemplateEditCallout"
>
<FormattedMessage
id="xpack.idxMgmt.templateEdit.systemTemplateWarningTitle"
defaultMessage="Editing a system template can break Kibana"
id="xpack.idxMgmt.templateEdit.systemTemplateWarningDescription"
defaultMessage="System templates are critical for internal operations."
/>
}
color="danger"
iconType="alert"
data-test-subj="systemTemplateEditCallout"
>
<FormattedMessage
id="xpack.idxMgmt.templateEdit.systemTemplateWarningDescription"
defaultMessage="System templates are critical for internal operations."
/>
</EuiCallOut>
<EuiSpacer size="l" />
</Fragment>
)}
<TemplateForm
template={template}
onSave={onSave}
isSaving={isSaving}
saveError={saveError}
clearSaveError={clearSaveError}
isEditing={true}
/>
</Fragment>
);
</EuiCallOut>
<EuiSpacer size="l" />
</Fragment>
)}
<TemplateForm
template={template}
onSave={onSave}
isSaving={isSaving}
saveError={saveError}
clearSaveError={clearSaveError}
isEditing={true}
/>
</Fragment>
);
}
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

// Cloud has its own system for managing templates and we want to make
// this clear in the UI when a template is used in a Cloud deployment.
export const getManagedTemplatePrefix = async (
callWithInternalUser: any
): Promise<string | undefined> => {
try {
const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', {
filterPath: '*.*managed_index_templates',
flatSettings: true,
includeDefaults: true,
});

const { 'cluster.metadata.managed_index_templates': managedTemplatesPrefix = undefined } = {
...defaults,
...persistent,
...transient,
};
return managedTemplatesPrefix;
} catch (e) {
// Silently swallow error and return undefined for the prefix
// so that downstream calls are not blocked.
return;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,34 @@

import { deserializeTemplate, deserializeTemplateList } from '../../../../common/lib';
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { getManagedTemplatePrefix } from '../../../lib/get_managed_templates';

let callWithInternalUser: any;

const allHandler: RouterRouteHandler = async (_req, callWithRequest) => {
const managedTemplatePrefix = await getManagedTemplatePrefix(callWithInternalUser);

const indexTemplatesByName = await callWithRequest('indices.getTemplate');

return deserializeTemplateList(indexTemplatesByName);
return deserializeTemplateList(indexTemplatesByName, managedTemplatePrefix);
};

const oneHandler: RouterRouteHandler = async (req, callWithRequest) => {
const { name } = req.params;
const managedTemplatePrefix = await getManagedTemplatePrefix(callWithInternalUser);
const indexTemplateByName = await callWithRequest('indices.getTemplate', { name });

if (indexTemplateByName[name]) {
return deserializeTemplate({ ...indexTemplateByName[name], name });
return deserializeTemplate({ ...indexTemplateByName[name], name }, managedTemplatePrefix);
}
};

export function registerGetAllRoute(router: Router) {
export function registerGetAllRoute(router: Router, server: any) {
callWithInternalUser = server.plugins.elasticsearch.getCluster('data').callWithInternalUser;
router.get('templates', allHandler);
}

export function registerGetOneRoute(router: Router) {
export function registerGetOneRoute(router: Router, server: any) {
callWithInternalUser = server.plugins.elasticsearch.getCluster('data').callWithInternalUser;
router.get('templates/{name}', oneHandler);
}
Loading

0 comments on commit a6335fd

Please sign in to comment.