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, }, }; }