diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index b1f343bc6ff7d..cb20e89dc2a1f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -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
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js b/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js
index e47a3ae329a91..99143b0a4ed96 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js
@@ -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()) {
+ // 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: [],
diff --git a/x-pack/plugins/snapshot_restore/common/constants.ts b/x-pack/plugins/snapshot_restore/common/constants.ts
index 7335dd1a72968..2ab13e71a118a 100644
--- a/x-pack/plugins/snapshot_restore/common/constants.ts
+++ b/x-pack/plugins/snapshot_restore/common/constants.ts
@@ -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'];
diff --git a/x-pack/plugins/snapshot_restore/plugin.ts b/x-pack/plugins/snapshot_restore/plugin.ts
index 74adb0c7d0ae1..35ef05f91be8e 100644
--- a/x-pack/plugins/snapshot_restore/plugin.ts
+++ b/x-pack/plugins/snapshot_restore/plugin.ts
@@ -12,6 +12,6 @@ export class Plugin {
const router = core.http.createRouter(API_BASE_PATH);
// Register routes
- registerRoutes(router);
+ registerRoutes(router, plugins);
}
}
diff --git a/x-pack/plugins/snapshot_restore/public/app/app.tsx b/x-pack/plugins/snapshot_restore/public/app/app.tsx
index 9ae963bf6750d..a6e2756f82d7d 100644
--- a/x-pack/plugins/snapshot_restore/public/app/app.tsx
+++ b/x-pack/plugins/snapshot_restore/public/app/app.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ if (permissionsError) {
+ return (
+
+ }
+ error={permissionsError}
+ />
+ );
+ }
+
+ if (!hasPermission) {
+ return (
+
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+
+ );
+ }
-export const App = () => {
return (
diff --git a/x-pack/plugins/snapshot_restore/public/app/components/repository_form/step_one.tsx b/x-pack/plugins/snapshot_restore/public/app/components/repository_form/step_one.tsx
index d102052749c45..fe20b9276ae6c 100644
--- a/x-pack/plugins/snapshot_restore/public/app/components/repository_form/step_one.tsx
+++ b/x-pack/plugins/snapshot_restore/public/app/components/repository_form/step_one.tsx
@@ -75,6 +75,15 @@ export const RepositoryFormStepOne: React.FunctionComponent = ({
}
};
+ const pluginDocLink = (
+
+
+
+ );
+
const renderNameField = () => (
= ({
onTypeChange(type);
}
}}
- grow={1}
>
= ({
);
}
+ if (!repositoryTypes.length) {
+ return (
+
+ }
+ color="warning"
+ data-test-subj="noRepositoryTypesError"
+ >
+
+
+ );
+ }
+
return (
{repositoryTypes.map((type: RepositoryType, index: number) => renderTypeCard(type, index))}
@@ -211,26 +242,25 @@ export const RepositoryFormStepOne: React.FunctionComponent = ({
}
description={
-
+ repositoryTypes.includes(REPOSITORY_TYPES.fs) &&
+ repositoryTypes.includes(REPOSITORY_TYPES.url) ? (
-
-
- ),
+ docLink: pluginDocLink,
+ }}
+ />
+ ) : (
+
-
+ )
}
idAria="repositoryTypeDescription"
fullWidth
diff --git a/x-pack/plugins/snapshot_restore/public/app/services/http/app_requests.ts b/x-pack/plugins/snapshot_restore/public/app/services/http/app_requests.ts
new file mode 100644
index 0000000000000..9d7144b22afe2
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/app/services/http/app_requests.ts
@@ -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',
+ });
+};
diff --git a/x-pack/plugins/snapshot_restore/public/app/services/http/index.ts b/x-pack/plugins/snapshot_restore/public/app/services/http/index.ts
index d8eda129b6966..5677f697db86e 100644
--- a/x-pack/plugins/snapshot_restore/public/app/services/http/index.ts
+++ b/x-pack/plugins/snapshot_restore/public/app/services/http/index.ts
@@ -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';
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts
new file mode 100644
index 0000000000000..7a784fb5d0198
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts
@@ -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,
+ };
+};
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/register_routes.ts b/x-pack/plugins/snapshot_restore/server/routes/api/register_routes.ts
index 726d52efed2f2..73e70f3316356 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/register_routes.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/register_routes.ts
@@ -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);
};
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts
index e694eb999096d..ebbd76f2d50c2 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts
@@ -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);
@@ -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))];
diff --git a/x-pack/plugins/snapshot_restore/shim.ts b/x-pack/plugins/snapshot_restore/shim.ts
index 186da1a39d3d9..5f61828311541 100644
--- a/x-pack/plugins/snapshot_restore/shim.ts
+++ b/x-pack/plugins/snapshot_restore/shim.ts
@@ -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';
@@ -22,6 +23,12 @@ export interface Plugins {
license: {
registerLicenseChecker: typeof registerLicenseChecker;
};
+ cloud: {
+ config: {
+ isCloudEnabled: boolean;
+ };
+ };
+ xpack_main: any;
}
export function createShim(
@@ -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,
},
};
}