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
6 changes: 5 additions & 1 deletion x-pack/legacy/plugins/snapshot_restore/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,8 @@ export const REPOSITORY_PLUGINS_MAP: { [key: string]: RepositoryType } = {
'repository-gcs': REPOSITORY_TYPES.gcs,
};

export const APP_PERMISSIONS = ['create_snapshot', 'cluster:admin/repository'];
export const APP_REQUIRED_CLUSTER_PRIVILEGES = [
'cluster:admin/snapshot',
'cluster:admin/repository',
];
export const APP_RESTORE_INDEX_PRIVILEGES = ['monitor'];
11 changes: 11 additions & 0 deletions x-pack/legacy/plugins/snapshot_restore/common/types/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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.
*/

export interface AppPermissions {
hasPermission: boolean;
missingClusterPrivileges: string[];
missingIndexPrivileges: string[];
}
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.
*/

export * from './app';
export * from './repository';
export * from './snapshot';
export * from './restore';
32 changes: 27 additions & 5 deletions x-pack/legacy/plugins/snapshot_restore/public/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import React, { useEffect, useRef } 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, Section } from './constants';
import { RepositoryAdd, RepositoryEdit, RestoreSnapshot, SnapshotRestoreHome } from './sections';
import { useLoadPermissions } from './services/http';
import { useAppState } from './services/state';
import { useAppDependencies } from './index';

export const App: React.FunctionComponent = () => {
Expand All @@ -21,16 +22,37 @@ export const App: React.FunctionComponent = () => {
},
} = useAppDependencies();

// Get app state to set permissions data
const [, dispatch] = useAppState();

// Use ref for default permission data so that re-rendering doesn't
// cause dispatch to be called over and over
const defaultPermissionsData = useRef({
hasPermission: true,
missingClusterPrivileges: [],
missingIndexPrivileges: [],
});

// Load permissions
const {
error: permissionsError,
loading: loadingPermissions,
data: { hasPermission, missingClusterPrivileges } = {
hasPermission: true,
missingClusterPrivileges: [],
},
data: permissionsData = defaultPermissionsData.current,
} = useLoadPermissions();

const { hasPermission, missingClusterPrivileges } = permissionsData;

// Update app state with permissions data
useEffect(
() => {
dispatch({
type: 'updatePermissions',
permissions: permissionsData,
});
},
[permissionsData]
);

if (loadingPermissions) {
return (
<SectionLoading>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SectionError, SectionLoading } from '../../../components';
import { UIM_RESTORE_LIST_LOAD } from '../../../constants';
import { useAppDependencies } from '../../../index';
import { useLoadRestores } from '../../../services/http';
import { useAppState } from '../../../services/state';
import { uiMetricService } from '../../../services/ui_metric';
import { RestoreTable } from './restore_table';

Expand All @@ -42,6 +43,40 @@ export const RestoreList: React.FunctionComponent = () => {
},
} = useAppDependencies();

// Check that we have all index privileges needed to view recovery information
const [appState] = useAppState();
const { permissions: { missingIndexPrivileges } = { missingIndexPrivileges: [] } } = appState;

// Render permission missing screen
if (missingIndexPrivileges.length) {
return (
<EuiEmptyPrompt
iconType="securityApp"
title={
<h2>
<FormattedMessage
id="xpack.snapshotRestore.restoreList.deniedPermissionTitle"
defaultMessage="You're missing index privileges"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.snapshotRestore.restoreList.deniedPermissionDescription"
defaultMessage="To view snapshot restore status, you must have {indexPrivilegesCount,
plural, one {this index privilege} other {these index privileges}} for one or more indices: {indexPrivileges}."
values={{
indexPrivileges: missingIndexPrivileges.join(', '),
indexPrivilegesCount: missingIndexPrivileges.length,
}}
/>
</p>
}
/>
);
}

// State for tracking interval picker
const [isIntervalMenuOpen, setIsIntervalMenuOpen] = useState<boolean>(false);
const [currentInterval, setCurrentInterval] = useState<number>(INTERVAL_OPTIONS[1]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export const useRequest = ({
return;
}

// Only set data if we are doing polling
// Set just data if we are doing polling
if (isPollRequest) {
setPolling(false);
if (response.data) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { createContext, useContext } from 'react';
import { AppState } from '../../types';
import { createContext, useContext, Dispatch, ReducerAction } from 'react';
import { AppState, AppAction } from '../../types';

const StateContext = createContext<AppState>({});
type StateReducer = (state: AppState, action: AppAction) => AppState;
type ReducedStateContext = [AppState, Dispatch<ReducerAction<StateReducer>>];

export const initialState = {};
export const initialState: AppState = {
permissions: {
hasPermission: true,
missingClusterPrivileges: [],
missingIndexPrivileges: [],
},
};

export const reducer = (state: any, action: { type: string }) => {
switch (action.type) {
export const reducer: StateReducer = (state, action) => {
const { type, permissions } = action;
switch (type) {
case 'updatePermissions':
return {
...state,
permissions,
};
default:
return state;
}
};

const StateContext = createContext<ReducedStateContext>([initialState, () => {}]);

export const AppStateProvider = StateContext.Provider;

export const useAppState = () => useContext<AppState>(StateContext);
export const useAppState = () => useContext<ReducedStateContext>(StateContext);
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { AppPermissions } from '../../../common/types';
import { AppCore, AppPlugins } from '../../shim';
export { AppCore, AppPlugins } from '../../shim';

Expand All @@ -12,5 +13,7 @@ export interface AppDependencies {
}

export interface AppState {
[key: string]: any;
permissions: AppPermissions;
}

export type AppAction = { type: string } & { permissions: AppState['permissions'] };
78 changes: 58 additions & 20 deletions x-pack/legacy/plugins/snapshot_restore/server/routes/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
*/
import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router';
import { wrapCustomError } from '../../../../../server/lib/create_router/error_wrappers';
import { APP_PERMISSIONS } from '../../../common/constants';
import {
APP_REQUIRED_CLUSTER_PRIVILEGES,
APP_RESTORE_INDEX_PRIVILEGES,
} from '../../../common/constants';
import { AppPermissions } from '../../../common/types';
import { Plugins } from '../../../shim';

let xpackMainPlugin: any;
Expand All @@ -19,43 +23,77 @@ export function getXpackMainPlugin() {
return xpackMainPlugin;
}

export const getPermissionsHandler: RouterRouteHandler = async (req, callWithRequest) => {
const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean }): string[] => {
return Object.keys(privilegesObject).reduce(
(privileges: string[], privilegeName: string): string[] => {
if (!privilegesObject[privilegeName]) {
privileges.push(privilegeName);
}
return privileges;
},
[]
);
};

export const getPermissionsHandler: RouterRouteHandler = async (
req,
callWithRequest
): Promise<AppPermissions> => {
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 permissionsResult: AppPermissions = {
hasPermission: true,
missingClusterPrivileges: [],
missingIndexPrivileges: [],
};

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: [],
};
return permissionsResult;
}

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

const missingClusterPrivileges = Object.keys(cluster).reduce(
(permissions: string[], permissionName: string): string[] => {
if (!cluster[permissionName]) {
permissions.push(permissionName);
}
return permissions;
},
[]
);
// Find missing cluster privileges and set overall app permissions
permissionsResult.missingClusterPrivileges = extractMissingPrivileges(cluster || {});
permissionsResult.hasPermission = hasPermission;

return {
hasPermission,
missingClusterPrivileges,
};
// Get all index privileges the user has
const { indices } = await callWithRequest('transport.request', {
path: '/_security/user/_privileges',
method: 'GET',
});

// Check if they have all the required index privileges for at least one index
const oneIndexWithAllPrivileges = indices.find(({ privileges }: { privileges: string[] }) => {
if (privileges.includes('all')) {
return true;
}

const indexHasAllPrivileges = APP_RESTORE_INDEX_PRIVILEGES.every(privilege =>
privileges.includes(privilege)
);

return indexHasAllPrivileges;
});

// If they don't, return list of required index privileges
if (!oneIndexWithAllPrivileges) {
permissionsResult.missingIndexPrivileges = [...APP_RESTORE_INDEX_PRIVILEGES];
}

return permissionsResult;
};