Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
/x-pack/plugins/remote_clusters/ @elastic/es-ui
/x-pack/plugins/rollup/ @elastic/es-ui
/x-pack/plugins/searchprofiler/ @elastic/es-ui
/x-pack/plugins/snapshot_restore/ @elastic/es-ui
/x-pack/plugins/watcher/ @elastic/es-ui

# Kibana TSVB external contractors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export const registerCcrRoutes = (server) => {
}

const securityInfo = (xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'));
if (!securityInfo || !securityInfo.isEnabled()) {
// If security isn't enabled, let the user use CCR.
if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) {
Copy link
Copy Markdown
Contributor Author

@jen-huang jen-huang Apr 30, 2019

Choose a reason for hiding this comment

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

@cjcenizal I discovered that this branch isn't entered if security is enabled, but not available. This can happen if user has a license with security enabled, and reverts back to Basic. Basic doesn't have security available, but security is still "enabled".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great catch! And thanks for adding the comment. I'm sure I'll completely forget about this and that comment will save the day for me.

// If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR.
return {
hasPermission: true,
missingClusterPrivileges: [],
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/snapshot_restore/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ export const REPOSITORY_PLUGINS_MAP: { [key: string]: RepositoryType } = {
'repository-azure': REPOSITORY_TYPES.azure,
'repository-gcs': REPOSITORY_TYPES.gcs,
};

export const APP_PERMISSIONS = ['monitor', 'create_snapshot', 'cluster:admin/repository'];
2 changes: 1 addition & 1 deletion x-pack/plugins/snapshot_restore/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ export class Plugin {
const router = core.http.createRouter(API_BASE_PATH);

// Register routes
registerRoutes(router);
registerRoutes(router, plugins);
}
}
78 changes: 77 additions & 1 deletion x-pack/plugins/snapshot_restore/public/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,87 @@

import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui';

import { SectionLoading, SectionError } from './components';
import { BASE_PATH, DEFAULT_SECTION } from './constants';
import { RepositoryAdd, RepositoryEdit, SnapshotRestoreHome } from './sections';
import { loadPermissions } from './services/http';
import { useAppDependencies } from './index';

export const App: React.FunctionComponent = () => {
const {
core: {
i18n: { FormattedMessage },
},
} = useAppDependencies();

// Load permissions
const {
error: permissionsError,
loading: loadingPermissions,
data: { hasPermission, missingClusterPrivileges } = {
hasPermission: true,
missingClusterPrivileges: [],
},
} = loadPermissions();

if (loadingPermissions) {
return (
<SectionLoading>
<FormattedMessage
id="xpack.snapshotRestore.app.checkingPermissionsDescription"
defaultMessage="Checking permissions…"
/>
</SectionLoading>
);
}

if (permissionsError) {
return (
<SectionError
title={
<FormattedMessage
id="xpack.snapshotRestore.app.checkingPermissionsErrorMessage"
defaultMessage="Error checking permissions"
/>
}
error={permissionsError}
/>
);
}

if (!hasPermission) {
return (
<EuiPageContent horizontalPosition="center">
<EuiEmptyPrompt
iconType="securityApp"
title={
<h2>
<FormattedMessage
id="xpack.snapshotRestore.app.deniedPermissionTitle"
defaultMessage="You're missing cluster privileges"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.snapshotRestore.app.deniedPermissionDescription"
defaultMessage="To use Snapshot Repositories, you must have {clusterPrivilegesCount,
plural, one {this cluster privilege} other {these cluster privileges}}: {clusterPrivileges}."
values={{
clusterPrivileges: missingClusterPrivileges.join(', '),
clusterPrivilegesCount: missingClusterPrivileges.length,
}}
/>
</p>
}
/>
</EuiPageContent>
);
}

export const App = () => {
return (
<div>
<Switch>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
}
};

const pluginDocLink = (
<EuiLink href={documentationLinksService.getRepositoryPluginDocUrl()} target="_blank">
<FormattedMessage
id="xpack.snapshotRestore.repositoryForm.fields.typePluginsDocLinkText"
defaultMessage="Learn more about plugins."
/>
</EuiLink>
);

const renderNameField = () => (
<EuiDescribedFormGroup
title={
Expand Down Expand Up @@ -139,7 +148,6 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
onTypeChange(type);
}
}}
grow={1}
>
<EuiCard
className={`ssrRepositoryFormTypeCard
Expand Down Expand Up @@ -190,6 +198,29 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
);
}

if (!repositoryTypes.length) {
return (
<EuiCallOut
title={
<FormattedMessage
id="xpack.snapshotRestore.repositoryForm.noRepositoryTypesErrorTitle"
defaultMessage="No repository types available"
/>
}
color="warning"
data-test-subj="noRepositoryTypesError"
>
<FormattedMessage
id="xpack.snapshotRestore.repositoryForm.noRepositoryTypesErrorMessage"
defaultMessage="You can install plugins to enable different repository types. {docLink}"
values={{
docLink: pluginDocLink,
}}
/>
</EuiCallOut>
);
}

return (
<EuiFlexGrid columns={3}>
{repositoryTypes.map((type: RepositoryType, index: number) => renderTypeCard(type, index))}
Expand All @@ -211,26 +242,25 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
</EuiTitle>
}
description={
<Fragment>
repositoryTypes.includes(REPOSITORY_TYPES.fs) &&
repositoryTypes.includes(REPOSITORY_TYPES.url) ? (
<FormattedMessage
id="xpack.snapshotRestore.repositoryForm.fields.typeDescription"
defaultMessage="Elasticsearch supports file system, read-only URL, and source-only repositories.
id="xpack.snapshotRestore.repositoryForm.fields.defaultTypeDescription"
defaultMessage="Elasticsearch supports file system and read-only URL repositories.
Additional types require plugins. {docLink}"
values={{
docLink: (
<EuiLink
href={documentationLinksService.getRepositoryPluginDocUrl()}
target="_blank"
>
<FormattedMessage
id="xpack.snapshotRestore.repositoryForm.fields.typePluginsDocLinkText"
defaultMessage="Learn more about plugins."
/>
</EuiLink>
),
docLink: pluginDocLink,
}}
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.repositoryForm.fields.cloudTypeDescription"
defaultMessage="Elasticsearch provides core plugins for custom repositories. {docLink}"
values={{
docLink: pluginDocLink,
}}
/>
</Fragment>
)
}
idAria="repositoryTypeDescription"
fullWidth
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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.
*/
import { API_BASE_PATH } from '../../../../common/constants';
import { httpService } from './http';
import { useRequest } from './use_request';

export const loadPermissions = () => {
return useRequest({
path: httpService.addBasePath(`${API_BASE_PATH}permissions`),
method: 'get',
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { httpService } from './http';
export * from './app_requests';
export * from './repository_requests';
export * from './snapshot_requests';
61 changes: 61 additions & 0 deletions x-pack/plugins/snapshot_restore/server/routes/api/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.
*/
import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router';
import { wrapCustomError } from '../../../../../server/lib/create_router/error_wrappers';
import { APP_PERMISSIONS } from '../../../common/constants';
import { Plugins } from '../../../shim';

let xpackMainPlugin: any;

export function registerAppRoutes(router: Router, plugins: Plugins) {
xpackMainPlugin = plugins.xpack_main;
router.get('permissions', getPermissionsHandler);
}

export function getXpackMainPlugin() {
return xpackMainPlugin;
}

export const getPermissionsHandler: RouterRouteHandler = async (req, callWithRequest) => {
const xpackInfo = getXpackMainPlugin() && getXpackMainPlugin().info;
if (!xpackInfo) {
// xpackInfo is updated via poll, so it may not be available until polling has begun.
// In this rare situation, tell the client the service is temporarily unavailable.
throw wrapCustomError(new Error('Security info unavailable'), 503);
}

const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security');
if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) {
// If security isn't enabled, let the user use app.
return {
hasPermission: true,
missingClusterPrivileges: [],
};
}

const { has_all_requested: hasPermission, cluster } = await callWithRequest('transport.request', {
path: '/_security/user/_has_privileges',
method: 'POST',
body: {
cluster: APP_PERMISSIONS,
},
});

const missingClusterPrivileges = Object.keys(cluster).reduce(
(permissions: string[], permissionName: string): string[] => {
if (!cluster[permissionName]) {
permissions.push(permissionName);
}
return permissions;
},
[]
);

return {
hasPermission,
missingClusterPrivileges,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Router } from '../../../../../server/lib/create_router';
import { Plugins } from '../../../shim';
import { registerAppRoutes } from './app';
import { registerRepositoriesRoutes } from './repositories';
import { registerSnapshotsRoutes } from './snapshots';

export const registerRoutes = (router: Router): void => {
registerRepositoriesRoutes(router);
export const registerRoutes = (router: Router, plugins: Plugins): void => {
registerAppRoutes(router, plugins);
registerRepositoriesRoutes(router, plugins);
registerSnapshotsRoutes(router);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import {
import { DEFAULT_REPOSITORY_TYPES, REPOSITORY_PLUGINS_MAP } from '../../../common/constants';
import { Repository, RepositoryType, RepositoryVerification } from '../../../common/types';

import { Plugins } from '../../../shim';
import { deserializeRepositorySettings, serializeRepositorySettings } from '../../lib';

export function registerRepositoriesRoutes(router: Router) {
let isCloudEnabled = false;

export function registerRepositoriesRoutes(router: Router, plugins: Plugins) {
isCloudEnabled = plugins.cloud.config.isCloudEnabled;
router.get('repository_types', getTypesHandler);
router.get('repositories', getAllHandler);
router.get('repositories/{name}', getOneHandler);
Expand Down Expand Up @@ -119,7 +123,8 @@ export const getOneHandler: RouterRouteHandler = async (
};

export const getTypesHandler: RouterRouteHandler = async (req, callWithRequest) => {
const types: RepositoryType[] = [...DEFAULT_REPOSITORY_TYPES];
// In ECE/ESS, do not enable the default types
const types: RepositoryType[] = isCloudEnabled ? [] : [...DEFAULT_REPOSITORY_TYPES];
const plugins: any[] = await callWithRequest('cat.plugins', { format: 'json' });
if (plugins && plugins.length) {
const pluginNames: string[] = [...new Set(plugins.map(plugin => plugin.component))];
Expand Down
13 changes: 13 additions & 0 deletions x-pack/plugins/snapshot_restore/shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { Legacy } from 'kibana';
import { createRouter, Router } from '../../server/lib/create_router';
Expand All @@ -22,6 +23,12 @@ export interface Plugins {
license: {
registerLicenseChecker: typeof registerLicenseChecker;
};
cloud: {
config: {
isCloudEnabled: boolean;
};
};
xpack_main: any;
}

export function createShim(
Expand All @@ -39,6 +46,12 @@ export function createShim(
license: {
registerLicenseChecker,
},
cloud: {
config: {
isCloudEnabled: get(server.plugins, 'cloud.config.isCloudEnabled', false),
},
},
xpack_main: server.plugins.xpack_main,
},
};
}