Skip to content

Commit

Permalink
[SR] Adjust app permissions (#39966)
Browse files Browse the repository at this point in the history
* Adjust SR app permissions for new features

* Fix app state and reducer typings

* Fix i18n key

* Fix typos

* Make logic more clear
  • Loading branch information
jen-huang authored Jul 1, 2019
1 parent 5753888 commit f60b536
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 35 deletions.
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;
};

0 comments on commit f60b536

Please sign in to comment.