mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
This commit is contained in:
parent
8b1245c3ae
commit
738e1fbcbc
16 changed files with 317 additions and 264 deletions
|
@ -4,7 +4,6 @@
|
|||
* 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';
|
||||
|
|
|
@ -4,16 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { EuiPageContent } 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';
|
||||
import { AuthorizationContext, WithPrivileges, NotAuthorizedSection } from './lib/authorization';
|
||||
|
||||
export const App: React.FunctionComponent = () => {
|
||||
const {
|
||||
|
@ -21,116 +20,82 @@ export const App: React.FunctionComponent = () => {
|
|||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = 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: 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>
|
||||
<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 and Restore, you must have {clusterPrivilegesCount,
|
||||
plural, one {this cluster privilege} other {these cluster privileges}}: {clusterPrivileges}."
|
||||
values={{
|
||||
clusterPrivileges: missingClusterPrivileges.join(', '),
|
||||
clusterPrivilegesCount: missingClusterPrivileges.length,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
);
|
||||
}
|
||||
const { apiError } = useContext(AuthorizationContext);
|
||||
|
||||
const sections: Section[] = ['repositories', 'snapshots', 'restore_status', 'policies'];
|
||||
const sectionsRegex = sections.join('|');
|
||||
|
||||
return (
|
||||
<div data-test-subj="snapshotRestoreApp">
|
||||
<Switch>
|
||||
<Route exact path={`${BASE_PATH}/add_repository`} component={RepositoryAdd} />
|
||||
<Route exact path={`${BASE_PATH}/edit_repository/:name*`} component={RepositoryEdit} />
|
||||
<Route
|
||||
exact
|
||||
path={`${BASE_PATH}/:section(${sectionsRegex})/:repositoryName?/:snapshotId*`}
|
||||
component={SnapshotRestoreHome}
|
||||
return apiError ? (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.app.checkingPrivilegesErrorMessage"
|
||||
defaultMessage="Error fetching user privileges from the server."
|
||||
/>
|
||||
<Redirect
|
||||
exact
|
||||
from={`${BASE_PATH}/restore/:repositoryName`}
|
||||
to={`${BASE_PATH}/snapshots`}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${BASE_PATH}/restore/:repositoryName/:snapshotId*`}
|
||||
component={RestoreSnapshot}
|
||||
/>
|
||||
<Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}/${DEFAULT_SECTION}`} />
|
||||
</Switch>
|
||||
</div>
|
||||
}
|
||||
error={apiError}
|
||||
/>
|
||||
) : (
|
||||
<WithPrivileges privileges="cluster.*">
|
||||
{({ isLoading, hasPrivileges, privilegesMissing }) =>
|
||||
isLoading ? (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.app.checkingPrivilegesDescription"
|
||||
defaultMessage="Checking privileges…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
) : hasPrivileges ? (
|
||||
<div data-test-subj="snapshotRestoreApp">
|
||||
<Switch>
|
||||
<Route exact path={`${BASE_PATH}/add_repository`} component={RepositoryAdd} />
|
||||
<Route
|
||||
exact
|
||||
path={`${BASE_PATH}/edit_repository/:name*`}
|
||||
component={RepositoryEdit}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${BASE_PATH}/:section(${sectionsRegex})/:repositoryName?/:snapshotId*`}
|
||||
component={SnapshotRestoreHome}
|
||||
/>
|
||||
<Redirect
|
||||
exact
|
||||
from={`${BASE_PATH}/restore/:repositoryName`}
|
||||
to={`${BASE_PATH}/snapshots`}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${BASE_PATH}/restore/:repositoryName/:snapshotId*`}
|
||||
component={RestoreSnapshot}
|
||||
/>
|
||||
<Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}/${DEFAULT_SECTION}`} />
|
||||
</Switch>
|
||||
</div>
|
||||
) : (
|
||||
<EuiPageContent>
|
||||
<NotAuthorizedSection
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.app.deniedPrivilegeTitle"
|
||||
defaultMessage="You're missing cluster privileges"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.app.deniedPrivilegeDescription"
|
||||
defaultMessage="To use Snapshot and Restore, you must have {privilegesCount,
|
||||
plural, one {this cluster privilege} other {these cluster privileges}}: {missingPrivileges}."
|
||||
values={{
|
||||
missingPrivileges: privilegesMissing.cluster!.join(', '),
|
||||
privilegesCount: privilegesMissing.cluster!.length,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
)
|
||||
}
|
||||
</WithPrivileges>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,12 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
import { App } from './app';
|
||||
import { AppStateProvider, initialState, reducer } from './services/state';
|
||||
import { httpService } from './services/http';
|
||||
import { AuthorizationProvider } from './lib/authorization';
|
||||
import { AppCore, AppDependencies, AppPlugins } from './types';
|
||||
|
||||
export { BASE_PATH as CLIENT_BASE_PATH } from './constants';
|
||||
|
@ -41,13 +43,15 @@ const getAppProviders = (deps: AppDependencies) => {
|
|||
const AppDependenciesProvider = setAppDependencies(deps);
|
||||
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<I18nContext>
|
||||
<HashRouter>
|
||||
<AppDependenciesProvider value={deps}>
|
||||
<AppStateProvider value={useReducer(reducer, initialState)}>{children}</AppStateProvider>
|
||||
</AppDependenciesProvider>
|
||||
</HashRouter>
|
||||
</I18nContext>
|
||||
<AuthorizationProvider
|
||||
privilegesEndpoint={httpService.addBasePath(`${API_BASE_PATH}privileges`)}
|
||||
>
|
||||
<I18nContext>
|
||||
<HashRouter>
|
||||
<AppDependenciesProvider value={deps}>{children}</AppDependenciesProvider>
|
||||
</HashRouter>
|
||||
</I18nContext>
|
||||
</AuthorizationProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 React, { createContext } from 'react';
|
||||
import { useRequest } from '../../../services/http/use_request';
|
||||
|
||||
interface Authorization {
|
||||
isLoading: boolean;
|
||||
apiError: {
|
||||
data: {
|
||||
error: string;
|
||||
cause?: string[];
|
||||
message?: string;
|
||||
};
|
||||
} | null;
|
||||
privileges: Privileges;
|
||||
}
|
||||
|
||||
export interface Privileges {
|
||||
hasAllPrivileges: boolean;
|
||||
missingPrivileges: MissingPrivileges;
|
||||
}
|
||||
|
||||
export interface MissingPrivileges {
|
||||
[key: string]: string[] | undefined;
|
||||
}
|
||||
|
||||
const initialValue: Authorization = {
|
||||
isLoading: true,
|
||||
apiError: null,
|
||||
privileges: {
|
||||
hasAllPrivileges: true,
|
||||
missingPrivileges: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const AuthorizationContext = createContext<Authorization>(initialValue);
|
||||
|
||||
interface Props {
|
||||
privilegesEndpoint: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) => {
|
||||
const { loading, error, data: privilegesData } = useRequest({
|
||||
path: privilegesEndpoint,
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
const value = {
|
||||
isLoading: loading,
|
||||
privileges: loading ? { hasAllPrivileges: true, missingPrivileges: {} } : privilegesData,
|
||||
apiError: error ? error : null,
|
||||
};
|
||||
|
||||
return <AuthorizationContext.Provider value={value}>{children}</AuthorizationContext.Provider>;
|
||||
};
|
|
@ -4,8 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface AppPermissions {
|
||||
hasPermission: boolean;
|
||||
missingClusterPrivileges: string[];
|
||||
missingIndexPrivileges: string[];
|
||||
}
|
||||
export { AuthorizationProvider, AuthorizationContext, Privileges } from './authorization_provider';
|
||||
|
||||
export { WithPrivileges } from './with_privileges';
|
||||
|
||||
export { NotAuthorizedSection } from './not_authorized_section';
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
title: React.ReactNode;
|
||||
message: React.ReactNode | string;
|
||||
}
|
||||
|
||||
export const NotAuthorizedSection = ({ title, message }: Props) => (
|
||||
<EuiEmptyPrompt iconType="securityApp" title={<h2>{title}</h2>} body={<p>{message}</p>} />
|
||||
);
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { useContext } from 'react';
|
||||
|
||||
import { AuthorizationContext, MissingPrivileges } from './authorization_provider';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Each required privilege must have the format "section.privilege".
|
||||
* To indicate that *all* privileges from a section are required, we can use the asterix
|
||||
* e.g. "index.*"
|
||||
*/
|
||||
privileges: string | string[];
|
||||
children: (childrenProps: {
|
||||
isLoading: boolean;
|
||||
hasPrivileges: boolean;
|
||||
privilegesMissing: MissingPrivileges;
|
||||
}) => JSX.Element;
|
||||
}
|
||||
|
||||
type Privilege = [string, string];
|
||||
|
||||
const toArray = (value: string | string[]): string[] =>
|
||||
Array.isArray(value) ? (value as string[]) : ([value] as string[]);
|
||||
|
||||
export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => {
|
||||
const { isLoading, privileges } = useContext(AuthorizationContext);
|
||||
|
||||
const privilegesToArray: Privilege[] = toArray(requiredPrivileges).map(p => {
|
||||
const [section, privilege] = p.split('.');
|
||||
if (!privilege) {
|
||||
// Oh! we forgot to use the dot "." notation.
|
||||
throw new Error('Required privilege must have the format "section.privilege"');
|
||||
}
|
||||
return [section, privilege];
|
||||
});
|
||||
|
||||
const hasPrivileges = isLoading
|
||||
? false
|
||||
: privilegesToArray.every(privilege => {
|
||||
const [section, requiredPrivilege] = privilege;
|
||||
if (!privileges.missingPrivileges[section]) {
|
||||
// if the section does not exist in our missingPriviledges, everything is OK
|
||||
return true;
|
||||
}
|
||||
if (privileges.missingPrivileges[section]!.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (requiredPrivilege === '*') {
|
||||
// If length > 0 and we require them all... KO
|
||||
return false;
|
||||
}
|
||||
// If we require _some_ privilege, we make sure that the one
|
||||
// we require is *not* in the missingPrivilege array
|
||||
return !privileges.missingPrivileges[section]!.includes(requiredPrivilege);
|
||||
});
|
||||
|
||||
const privilegesMissing = privilegesToArray.reduce(
|
||||
(acc, [section, privilege]) => {
|
||||
if (privilege === '*') {
|
||||
acc[section] = privileges.missingPrivileges[section] || [];
|
||||
} else if (
|
||||
privileges.missingPrivileges[section] &&
|
||||
privileges.missingPrivileges[section]!.includes(privilege)
|
||||
) {
|
||||
const missing: string[] = acc[section] || [];
|
||||
acc[section] = [...missing, privilege];
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as MissingPrivileges
|
||||
);
|
||||
|
||||
return children({ isLoading, hasPrivileges, privilegesMissing });
|
||||
};
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { initialState, reducer, AppStateProvider, useAppState } from './app_state';
|
||||
export * from './components';
|
|
@ -21,10 +21,10 @@ 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 { linkToSnapshots } from '../../../services/navigation';
|
||||
import { RestoreTable } from './restore_table';
|
||||
import { WithPrivileges, NotAuthorizedSection } from '../../../lib/authorization';
|
||||
|
||||
const ONE_SECOND_MS = 1000;
|
||||
const TEN_SECONDS_MS = 10 * 1000;
|
||||
|
@ -45,40 +45,6 @@ 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]);
|
||||
|
@ -94,7 +60,7 @@ export const RestoreList: React.FunctionComponent = () => {
|
|||
trackUiMetric(UIM_RESTORE_LIST_LOAD);
|
||||
}, []);
|
||||
|
||||
let content;
|
||||
let content: JSX.Element;
|
||||
|
||||
if (loading) {
|
||||
content = (
|
||||
|
@ -231,5 +197,33 @@ export const RestoreList: React.FunctionComponent = () => {
|
|||
);
|
||||
}
|
||||
|
||||
return <section data-test-subj="restoreList">{content}</section>;
|
||||
return (
|
||||
<WithPrivileges privileges="index.*">
|
||||
{({ hasPrivileges, privilegesMissing }) =>
|
||||
hasPrivileges ? (
|
||||
<section data-test-subj="restoreList">{content}</section>
|
||||
) : (
|
||||
<NotAuthorizedSection
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.deniedPrivilegeTitle"
|
||||
defaultMessage="You're missing index privileges"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.deniedPrivilegeDescription"
|
||||
defaultMessage="To view snapshot restore status, you must have {privilegesCount,
|
||||
plural, one {this index privilege} other {these index privileges}} for one or more indices: {missingPrivileges}."
|
||||
values={{
|
||||
missingPrivileges: privilegesMissing.index!.join(', '),
|
||||
privilegesCount: privilegesMissing.index!.length,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</WithPrivileges>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* 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 useLoadPermissions = () => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}permissions`),
|
||||
method: 'get',
|
||||
});
|
||||
};
|
|
@ -4,7 +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';
|
||||
export * from './restore_requests';
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* 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 { createContext, useContext, Dispatch, ReducerAction } from 'react';
|
||||
import { AppState, AppAction } from '../../types';
|
||||
|
||||
type StateReducer = (state: AppState, action: AppAction) => AppState;
|
||||
type ReducedStateContext = [AppState, Dispatch<ReducerAction<StateReducer>>];
|
||||
|
||||
export const initialState: AppState = {
|
||||
permissions: {
|
||||
hasPermission: true,
|
||||
missingClusterPrivileges: [],
|
||||
missingIndexPrivileges: [],
|
||||
},
|
||||
};
|
||||
|
||||
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<ReducedStateContext>(StateContext);
|
|
@ -3,7 +3,6 @@
|
|||
* 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';
|
||||
|
||||
|
@ -11,9 +10,3 @@ export interface AppDependencies {
|
|||
core: AppCore;
|
||||
plugins: AppPlugins;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
permissions: AppPermissions;
|
||||
}
|
||||
|
||||
export type AppAction = { type: string } & { permissions: AppState['permissions'] };
|
||||
|
|
|
@ -9,36 +9,34 @@ import {
|
|||
APP_REQUIRED_CLUSTER_PRIVILEGES,
|
||||
APP_RESTORE_INDEX_PRIVILEGES,
|
||||
} from '../../../common/constants';
|
||||
import { AppPermissions } from '../../../common/types';
|
||||
// NOTE: now we import it from our "public" folder, but when the Authorisation lib
|
||||
// will move to the "es_ui_shared" plugin, it will be imported from its "static" folder
|
||||
import { Privileges } from '../../../public/app/lib/authorization';
|
||||
import { Plugins } from '../../../shim';
|
||||
|
||||
let xpackMainPlugin: any;
|
||||
|
||||
export function registerAppRoutes(router: Router, plugins: Plugins) {
|
||||
xpackMainPlugin = plugins.xpack_main;
|
||||
router.get('permissions', getPermissionsHandler);
|
||||
router.get('privileges', getPrivilegesHandler);
|
||||
}
|
||||
|
||||
export function getXpackMainPlugin() {
|
||||
return xpackMainPlugin;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
||||
const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = {}): string[] =>
|
||||
Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => {
|
||||
if (!privilegesObject[privilegeName]) {
|
||||
privileges.push(privilegeName);
|
||||
}
|
||||
return privileges;
|
||||
}, []);
|
||||
|
||||
export const getPermissionsHandler: RouterRouteHandler = async (
|
||||
export const getPrivilegesHandler: RouterRouteHandler = async (
|
||||
req,
|
||||
callWithRequest
|
||||
): Promise<AppPermissions> => {
|
||||
): Promise<Privileges> => {
|
||||
const xpackInfo = getXpackMainPlugin() && getXpackMainPlugin().info;
|
||||
if (!xpackInfo) {
|
||||
// xpackInfo is updated via poll, so it may not be available until polling has begun.
|
||||
|
@ -46,30 +44,35 @@ export const getPermissionsHandler: RouterRouteHandler = async (
|
|||
throw wrapCustomError(new Error('Security info unavailable'), 503);
|
||||
}
|
||||
|
||||
const permissionsResult: AppPermissions = {
|
||||
hasPermission: true,
|
||||
missingClusterPrivileges: [],
|
||||
missingIndexPrivileges: [],
|
||||
const privilegesResult: Privileges = {
|
||||
hasAllPrivileges: true,
|
||||
missingPrivileges: {
|
||||
cluster: [],
|
||||
index: [],
|
||||
},
|
||||
};
|
||||
|
||||
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 permissionsResult;
|
||||
return privilegesResult;
|
||||
}
|
||||
|
||||
// Get cluster priviliges
|
||||
const { has_all_requested: hasPermission, cluster } = await callWithRequest('transport.request', {
|
||||
path: '/_security/user/_has_privileges',
|
||||
method: 'POST',
|
||||
body: {
|
||||
cluster: APP_REQUIRED_CLUSTER_PRIVILEGES,
|
||||
},
|
||||
});
|
||||
const { has_all_requested: hasAllPrivileges, cluster } = await callWithRequest(
|
||||
'transport.request',
|
||||
{
|
||||
path: '/_security/user/_has_privileges',
|
||||
method: 'POST',
|
||||
body: {
|
||||
cluster: APP_REQUIRED_CLUSTER_PRIVILEGES,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Find missing cluster privileges and set overall app permissions
|
||||
permissionsResult.missingClusterPrivileges = extractMissingPrivileges(cluster || {});
|
||||
permissionsResult.hasPermission = hasPermission;
|
||||
// Find missing cluster privileges and set overall app privileges
|
||||
privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster);
|
||||
privilegesResult.hasAllPrivileges = hasAllPrivileges;
|
||||
|
||||
// Get all index privileges the user has
|
||||
const { indices } = await callWithRequest('transport.request', {
|
||||
|
@ -92,8 +95,8 @@ export const getPermissionsHandler: RouterRouteHandler = async (
|
|||
|
||||
// If they don't, return list of required index privileges
|
||||
if (!oneIndexWithAllPrivileges) {
|
||||
permissionsResult.missingIndexPrivileges = [...APP_RESTORE_INDEX_PRIVILEGES];
|
||||
privilegesResult.missingPrivileges.index = [...APP_RESTORE_INDEX_PRIVILEGES];
|
||||
}
|
||||
|
||||
return permissionsResult;
|
||||
return privilegesResult;
|
||||
};
|
||||
|
|
|
@ -9898,10 +9898,6 @@
|
|||
"xpack.snapshotRestore.addRepository.savingRepositoryErrorTitle": "新規レポジトリを登録できません",
|
||||
"xpack.snapshotRestore.addRepositoryButtonLabel": "レポジトリを登録",
|
||||
"xpack.snapshotRestore.addRepositoryTitle": "レポジトリの登録",
|
||||
"xpack.snapshotRestore.app.checkingPermissionsDescription": "パーミッションを確認中…",
|
||||
"xpack.snapshotRestore.app.checkingPermissionsErrorMessage": "パーミッションの確認中にエラーが発生",
|
||||
"xpack.snapshotRestore.app.deniedPermissionDescription": "スナップショットレポジトリを使用するには、{clusterPrivilegesCount, plural, one {このクラスター特権} other {これらのクラスター特権}}が必要です: {clusterPrivileges}。",
|
||||
"xpack.snapshotRestore.app.deniedPermissionTitle": "クラスター特権が足りません",
|
||||
"xpack.snapshotRestore.appName": "スナップショットリポジドリ",
|
||||
"xpack.snapshotRestore.dataPlaceholderLabel": "-",
|
||||
"xpack.snapshotRestore.deleteRepository.confirmModal.cancelButtonLabel": "キャンセル",
|
||||
|
|
|
@ -9898,10 +9898,6 @@
|
|||
"xpack.snapshotRestore.addRepository.savingRepositoryErrorTitle": "无法注册新存储库",
|
||||
"xpack.snapshotRestore.addRepositoryButtonLabel": "注册存储库",
|
||||
"xpack.snapshotRestore.addRepositoryTitle": "注册存储库",
|
||||
"xpack.snapshotRestore.app.checkingPermissionsDescription": "正在检查权限......",
|
||||
"xpack.snapshotRestore.app.checkingPermissionsErrorMessage": "检查权限时出错",
|
||||
"xpack.snapshotRestore.app.deniedPermissionDescription": "要使用“快照存储库”,必须具有{clusterPrivilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{clusterPrivileges}。",
|
||||
"xpack.snapshotRestore.app.deniedPermissionTitle": "您缺少集群权限",
|
||||
"xpack.snapshotRestore.appName": "快照存储库",
|
||||
"xpack.snapshotRestore.dataPlaceholderLabel": "-",
|
||||
"xpack.snapshotRestore.deleteRepository.confirmModal.cancelButtonLabel": "取消",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue