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

[IM] prevent users from editing and deleting cloud-managed templates #43901

Merged
merged 5 commits into from
Aug 28, 2019
Merged
Show file tree
Hide file tree
Changes from 2 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
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 @@ -105,30 +105,6 @@ export const TemplateDetails: React.FunctionComponent<Props> = ({
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 Down Expand Up @@ -229,7 +205,6 @@ export const TemplateDetails: React.FunctionComponent<Props> = ({
/>
</EuiButtonEmpty>
</EuiFlexItem>

{templateDetails && (
<EuiFlexItem grow={false}>
{/* Manage templates context menu */}
Expand Down Expand Up @@ -267,7 +242,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: templateDetails.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: templateDetails.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', {
Copy link
Contributor

Choose a reason for hiding this comment

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

Where does this message get surfaced?

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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { registerDeleteRoute } from './register_delete_route';
import { registerCreateRoute } from './register_create_route';
import { registerUpdateRoute } from './register_update_route';

export function registerTemplateRoutes(router: Router) {
registerGetAllRoute(router);
registerGetOneRoute(router);
export function registerTemplateRoutes(router: Router, server: any) {
registerGetAllRoute(router, server);
registerGetOneRoute(router, server);
registerDeleteRoute(router);
registerCreateRoute(router);
registerUpdateRoute(router);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const getTemplate = ({
settings,
aliases,
mappings,
isManaged = false,
}: Partial<Template> = {}): Template => ({
name,
version,
Expand All @@ -23,4 +24,5 @@ export const getTemplate = ({
settings,
aliases,
mappings,
isManaged,
});