Skip to content

Commit

Permalink
[Upgrade Assistant] Add permissions check to logs step (elastic#112420)
Browse files Browse the repository at this point in the history
  • Loading branch information
sabarasaba committed Oct 20, 2021
1 parent 60f66e9 commit 8de7475
Show file tree
Hide file tree
Showing 13 changed files with 294 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import { MAJOR_VERSION } from '../../../common/constants';
import { HttpSetup } from 'src/core/public';

import { AuthorizationContext, Authorization, Privileges } from '../../../public/shared_imports';
import { AppContextProvider } from '../../../public/application/app_context';
import { apiService } from '../../../public/application/lib/api';
import { breadcrumbService } from '../../../public/application/lib/breadcrumbs';
Expand All @@ -29,23 +30,34 @@ const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });

export const kibanaVersion = new SemVer(MAJOR_VERSION);

export const WithAppDependencies = (Comp: any, overrides: Record<string, unknown> = {}) => (
props: Record<string, unknown>
) => {
apiService.setup((mockHttpClient as unknown) as HttpSetup);
breadcrumbService.setup(() => '');

const appContextMock = (getAppContextMock() as unknown) as AppDependencies;

return (
<AppContextProvider value={merge(appContextMock, overrides)}>
<GlobalFlyoutProvider>
<Comp {...props} />
</GlobalFlyoutProvider>
</AppContextProvider>
);
const createAuthorizationContextValue = (privileges: Privileges) => {
return {
isLoading: false,
privileges: privileges ?? { hasAllPrivileges: false, missingPrivileges: {} },
} as Authorization;
};

export const WithAppDependencies =
(Comp: any, { privileges, ...overrides }: Record<string, unknown> = {}) =>
(props: Record<string, unknown>) => {
apiService.setup(mockHttpClient as unknown as HttpSetup);
breadcrumbService.setup(() => '');

const appContextMock = getAppContextMock() as unknown as AppDependencies;

return (
<AuthorizationContext.Provider
value={createAuthorizationContextValue(privileges as Privileges)}
>
<AppContextProvider value={merge(appContextMock, overrides)}>
<GlobalFlyoutProvider>
<Comp {...props} />
</GlobalFlyoutProvider>
</AppContextProvider>
</AuthorizationContext.Provider>
);
};

export const setupEnvironment = () => {
const { server, setServerAsync, httpRequestsMockHelpers } = initHttpRequests();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ jest.mock('../../../../public/application/lib/logs_checkpoint', () => {
});

import { DeprecationLoggingStatus } from '../../../../common/types';
import { DEPRECATION_LOGS_SOURCE_ID } from '../../../../common/constants';
import { OverviewTestBed, setupOverviewPage } from '../overview.helpers';
import { setupEnvironment, advanceTime } from '../../helpers';
import { DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS } from '../../../../common/constants';
import {
DEPRECATION_LOGS_INDEX,
DEPRECATION_LOGS_SOURCE_ID,
DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS,
} from '../../../../common/constants';

const getLoggingResponse = (toggle: boolean): DeprecationLoggingStatus => ({
isDeprecationLogIndexingEnabled: toggle,
Expand Down Expand Up @@ -389,4 +392,39 @@ describe('Overview - Fix deprecation logs step', () => {
expect(exists('apiCompatibilityNoteTitle')).toBe(true);
});
});

describe('Privileges check', () => {
test(`permissions warning callout is hidden if user has the right privileges`, async () => {
const { exists } = testBed;

// Index privileges warning callout should not be shown
expect(exists('noIndexPermissionsCallout')).toBe(false);
// Analyze logs and Resolve logs sections should be shown
expect(exists('externalLinksTitle')).toBe(true);
expect(exists('deprecationsCountTitle')).toBe(true);
});

test(`doesn't show analyze and resolve logs if it doesn't have the right privileges`, async () => {
await act(async () => {
testBed = await setupOverviewPage({
privileges: {
hasAllPrivileges: false,
missingPrivileges: {
index: [DEPRECATION_LOGS_INDEX],
},
},
});
});

const { exists, component } = testBed;

component.update();

// No index privileges warning callout should be shown
expect(exists('noIndexPermissionsCallout')).toBe(true);
// Analyze logs and Resolve logs sections should be hidden
expect(exists('externalLinksTitle')).toBe(false);
expect(exists('deprecationsCountTitle')).toBe(false);
});
});
});
21 changes: 12 additions & 9 deletions x-pack/plugins/upgrade_assistant/public/application/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { Router, Switch, Route, Redirect } from 'react-router-dom';
import { ScopedHistory } from 'src/core/public';

import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
import { APP_WRAPPER_CLASS, GlobalFlyout } from '../shared_imports';
import { APP_WRAPPER_CLASS, GlobalFlyout, AuthorizationProvider } from '../shared_imports';
import { AppDependencies } from '../types';
import { API_BASE_PATH } from '../../common/constants';
import { AppContextProvider, useAppContext } from './app_context';
import { EsDeprecations, ComingSoonPrompt, KibanaDeprecations, Overview } from './components';

Expand Down Expand Up @@ -46,18 +47,20 @@ export const AppWithRouter = ({ history }: { history: ScopedHistory }) => {
export const RootComponent = (dependencies: AppDependencies) => {
const {
history,
core: { i18n, application },
core: { i18n, application, http },
} = dependencies.services;

return (
<RedirectAppLinks application={application} className={APP_WRAPPER_CLASS}>
<i18n.Context>
<AppContextProvider value={dependencies}>
<GlobalFlyoutProvider>
<AppWithRouter history={history} />
</GlobalFlyoutProvider>
</AppContextProvider>
</i18n.Context>
<AuthorizationProvider httpClient={http} privilegesEndpoint={`${API_BASE_PATH}/privileges`}>
<i18n.Context>
<AppContextProvider value={dependencies}>
<GlobalFlyoutProvider>
<AppWithRouter history={history} />
</GlobalFlyoutProvider>
</AppContextProvider>
</i18n.Context>
</AuthorizationProvider>
</RedirectAppLinks>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ const i18nTexts = {
/>
),
calloutBody: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.calloutBody', {
defaultMessage:
'Reset the counter after making changes and continue monitoring to verify that you are no longer using deprecated APIs.',
defaultMessage: `After making changes, reset the counter and continue monitoring to verify you're no longer using deprecated features.`,
}),
loadingError: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.loadingError', {
defaultMessage: 'An error occurred while retrieving the count of deprecation logs',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { FunctionComponent, useState, useEffect } from 'react';

import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiText, EuiSpacer, EuiPanel, EuiLink, EuiCallOut } from '@elastic/eui';
import { EuiText, EuiSpacer, EuiPanel, EuiLink, EuiCallOut, EuiCode } from '@elastic/eui';
import type { EuiStepProps } from '@elastic/eui/src/components/steps/step';

import { useAppContext } from '../../../app_context';
Expand All @@ -19,6 +18,8 @@ import { useDeprecationLogging } from './use_deprecation_logging';
import { DeprecationLoggingToggle } from './deprecation_logging_toggle';
import { loadLogsCheckpoint, saveLogsCheckpoint } from '../../../lib/logs_checkpoint';
import type { OverviewStepProps } from '../../types';
import { DEPRECATION_LOGS_INDEX } from '../../../../../common/constants';
import { WithPrivileges, MissingPrivileges } from '../../../../shared_imports';

const i18nTexts = {
identifyStepTitle: i18n.translate('xpack.upgradeAssistant.overview.identifyStepTitle', {
Expand Down Expand Up @@ -71,13 +72,40 @@ const i18nTexts = {
'Go to your logs directory to view the deprecation logs or enable log collecting to see them in the UI.',
}
),
deniedPrivilegeTitle: i18n.translate(
'xpack.upgradeAssistant.overview.deprecationLogs.deniedPrivilegeTitle',
{
defaultMessage: 'You require index privileges to analyze the deprecation logs',
}
),
deniedPrivilegeDescription: (privilegesMissing: MissingPrivileges) => (
// NOTE: hardcoding the missing privilege because the WithPrivileges HOC
// doesnt provide a way to retrieve which specific privileges an index
// is missing.
<FormattedMessage
id="xpack.upgradeAssistant.overview.deprecationLogs.deniedPrivilegeDescription"
defaultMessage="The deprecation logs will continue to be indexed, but you won't be able to analyze them until you have the read index {privilegesCount, plural, one {privilege} other {privileges}} for: {missingPrivileges}"
values={{
missingPrivileges: (
<EuiCode transparentBackground={true}>{privilegesMissing?.index?.join(', ')}</EuiCode>
),
privilegesCount: privilegesMissing?.index?.length,
}}
/>
),
};

interface Props {
setIsComplete: OverviewStepProps['setIsComplete'];
hasPrivileges: boolean;
privilegesMissing: MissingPrivileges;
}

const FixLogsStep: FunctionComponent<Props> = ({ setIsComplete }) => {
const FixLogsStep: FunctionComponent<Props> = ({
setIsComplete,
hasPrivileges,
privilegesMissing,
}) => {
const state = useDeprecationLogging();
const {
services: {
Expand Down Expand Up @@ -123,7 +151,21 @@ const FixLogsStep: FunctionComponent<Props> = ({ setIsComplete }) => {
</>
)}

{state.isDeprecationLogIndexingEnabled && (
{!hasPrivileges && state.isDeprecationLogIndexingEnabled && (
<>
<EuiSpacer size="m" />
<EuiCallOut
iconType="help"
color="warning"
title={i18nTexts.deniedPrivilegeTitle}
data-test-subj="noIndexPermissionsCallout"
>
<p>{i18nTexts.deniedPrivilegeDescription(privilegesMissing)}</p>
</EuiCallOut>
</>
)}

{hasPrivileges && state.isDeprecationLogIndexingEnabled && (
<>
<EuiSpacer size="xl" />
<EuiText data-test-subj="externalLinksTitle">
Expand Down Expand Up @@ -168,6 +210,16 @@ export const getFixLogsStep = ({ isComplete, setIsComplete }: OverviewStepProps)
status,
title: i18nTexts.identifyStepTitle,
'data-test-subj': `fixLogsStep-${status}`,
children: <FixLogsStep setIsComplete={setIsComplete} />,
children: (
<WithPrivileges privileges={`index.${DEPRECATION_LOGS_INDEX}`}>
{({ hasPrivileges, privilegesMissing, isLoading }) => (
<FixLogsStep
setIsComplete={setIsComplete}
hasPrivileges={!isLoading && hasPrivileges}
privilegesMissing={privilegesMissing}
/>
)}
</WithPrivileges>
),
};
};
6 changes: 6 additions & 0 deletions x-pack/plugins/upgrade_assistant/public/shared_imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export {
UseRequestResponse,
SectionLoading,
GlobalFlyout,
WithPrivileges,
Privileges,
MissingPrivileges,
AuthorizationProvider,
AuthorizationContext,
Authorization,
} from '../../../../src/plugins/es_ui_shared/public/';

export { Storage } from '../../../../src/plugins/kibana_utils/public';
Expand Down
7 changes: 6 additions & 1 deletion x-pack/plugins/upgrade_assistant/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { SecurityPluginStart } from '../../security/server';
import { InfraPluginSetup } from '../../infra/server';

import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { SecurityPluginSetup } from '../../security/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX } from '../common/constants';

Expand All @@ -42,6 +43,7 @@ interface PluginsSetup {
licensing: LicensingPluginSetup;
features: FeaturesPluginSetup;
infra: InfraPluginSetup;
security?: SecurityPluginSetup;
}

interface PluginsStart {
Expand Down Expand Up @@ -76,7 +78,7 @@ export class UpgradeAssistantServerPlugin implements Plugin {

setup(
{ http, getStartServices, savedObjects }: CoreSetup,
{ usageCollection, features, licensing, infra }: PluginsSetup
{ usageCollection, features, licensing, infra, security }: PluginsSetup
) {
this.licensing = licensing;

Expand Down Expand Up @@ -129,6 +131,9 @@ export class UpgradeAssistantServerPlugin implements Plugin {
lib: {
handleEsError,
},
config: {
isSecurityEnabled: () => security !== undefined && security.license.isEnabled(),
},
};

// Initialize version service with current kibana version
Expand Down
80 changes: 80 additions & 0 deletions x-pack/plugins/upgrade_assistant/server/routes/app.ts
Original file line number Diff line number Diff line change
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { API_BASE_PATH, DEPRECATION_LOGS_INDEX } from '../../common/constants';
import { versionCheckHandlerWrapper } from '../lib/es_version_precheck';
import { Privileges } from '../shared_imports';
import { RouteDependencies } from '../types';

const extractMissingPrivileges = (
privilegesObject: { [key: string]: Record<string, boolean> } = {}
): string[] =>
Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => {
if (Object.values(privilegesObject[privilegeName]).some((e) => !e)) {
privileges.push(privilegeName);
}
return privileges;
}, []);

export function registerAppRoutes({
router,
lib: { handleEsError },
config: { isSecurityEnabled },
}: RouteDependencies) {
router.get(
{
path: `${API_BASE_PATH}/privileges`,
validate: false,
},
versionCheckHandlerWrapper(
async (
{
core: {
elasticsearch: { client },
},
},
request,
response
) => {
const privilegesResult: Privileges = {
hasAllPrivileges: true,
missingPrivileges: {
index: [],
},
};

if (!isSecurityEnabled()) {
return response.ok({ body: privilegesResult });
}

try {
const {
body: { has_all_requested: hasAllPrivileges, index },
} = await client.asCurrentUser.security.hasPrivileges({
body: {
index: [
{
names: [DEPRECATION_LOGS_INDEX],
privileges: ['read'],
},
],
},
});

if (!hasAllPrivileges) {
privilegesResult.missingPrivileges.index = extractMissingPrivileges(index);
}

privilegesResult.hasAllPrivileges = hasAllPrivileges;
return response.ok({ body: privilegesResult });
} catch (error) {
return handleEsError({ error, response });
}
}
)
);
}
Loading

0 comments on commit 8de7475

Please sign in to comment.