Skip to content

Commit

Permalink
[ML] Transforms: Fix privileges check. (elastic#163687)
Browse files Browse the repository at this point in the history
Fixes a regression introduced in elastic#154007.

When security is disabled, the check for transform
privileges/capabilities would fail and the transform page would not
render.

- This fixes the endpoint to return data in the correct format should
security be disabled.
- The endpoint's TypeScript has been updated to catch malformed
responses.
- The client side custom `useRequest` is replaced with `useQuery` from
`'@tanstack/react-query'` which also fixes TypeScript support on the
client side.

(cherry picked from commit ecaa8a7)

# Conflicts:
#	x-pack/plugins/transform/public/app/app.tsx
  • Loading branch information
walterra committed Aug 11, 2023
1 parent 5c5c2da commit 3be6115
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 99 deletions.
Expand Up @@ -12,6 +12,11 @@ import { cloneDeep } from 'lodash';
import { APP_INDEX_PRIVILEGES } from '../constants';
import { Privileges } from '../types/privileges';

export interface PrivilegesAndCapabilities {
privileges: Privileges;
capabilities: Capabilities;
}

export interface TransformCapabilities {
canGetTransform: boolean;
canDeleteTransform: boolean;
Expand Down Expand Up @@ -89,7 +94,7 @@ export const getPrivilegesAndCapabilities = (
clusterPrivileges: Record<string, boolean>,
hasOneIndexWithAllPrivileges: boolean,
hasAllPrivileges: boolean
) => {
): PrivilegesAndCapabilities => {
const privilegesResult: Privileges = {
hasAllPrivileges: true,
missingPrivileges: {
Expand Down
5 changes: 0 additions & 5 deletions x-pack/plugins/transform/public/__mocks__/shared_imports.ts
Expand Up @@ -8,11 +8,6 @@
// actual mocks
export const expandLiteralStrings = jest.fn();
export const XJsonMode = jest.fn();
export const useRequest = jest.fn(() => ({
isLoading: false,
error: null,
data: undefined,
}));
export const getSavedSearch = jest.fn();

// just passing through the reimports
Expand Down
28 changes: 15 additions & 13 deletions x-pack/plugins/transform/public/app/app.tsx
Expand Up @@ -11,11 +11,11 @@ import { Router, Switch } from 'react-router-dom';
import { Route } from '@kbn/shared-ux-router';

import { ScopedHistory } from '@kbn/core/public';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import { EuiErrorBoundary } from '@elastic/eui';

import { FormattedMessage } from '@kbn/i18n-react';

import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';

import { addInternalBasePath } from '../../common/constants';
Expand All @@ -24,7 +24,6 @@ import { SectionError } from './components';
import { SECTION_SLUG } from './common/constants';
import { AuthorizationContext, AuthorizationProvider } from './lib/authorization';
import { AppDependencies } from './app_dependencies';

import { CloneTransformSection } from './sections/clone_transform';
import { CreateTransformSection } from './sections/create_transform';
import { TransformManagementSection } from './sections/transform_management';
Expand Down Expand Up @@ -64,20 +63,23 @@ export const App: FC<{ history: ScopedHistory }> = ({ history }) => {

export const renderApp = (element: HTMLElement, appDependencies: AppDependencies) => {
const I18nContext = appDependencies.i18n.Context;
const queryClient = new QueryClient();

render(
<EuiErrorBoundary>
<KibanaThemeProvider theme$={appDependencies.theme.theme$}>
<KibanaContextProvider services={appDependencies}>
<AuthorizationProvider
privilegesEndpoint={{ path: addInternalBasePath(`privileges`), version: '1' }}
>
<I18nContext>
<App history={appDependencies.history} />
</I18nContext>
</AuthorizationProvider>
</KibanaContextProvider>
</KibanaThemeProvider>
<QueryClientProvider client={queryClient}>
<KibanaThemeProvider theme$={appDependencies.theme.theme$}>
<KibanaContextProvider services={appDependencies}>
<AuthorizationProvider
privilegesEndpoint={{ path: addInternalBasePath(`privileges`), version: '1' }}
>
<I18nContext>
<App history={appDependencies.history} />
</I18nContext>
</AuthorizationProvider>
</KibanaContextProvider>
</KibanaThemeProvider>
</QueryClientProvider>
</EuiErrorBoundary>,
element
);
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/transform/public/app/hooks/index.ts
Expand Up @@ -12,4 +12,3 @@ export { useResetTransforms } from './use_reset_transform';
export { useScheduleNowTransforms } from './use_schedule_now_transform';
export { useStartTransforms } from './use_start_transform';
export { useStopTransforms } from './use_stop_transform';
export { useRequest } from './use_request';
15 changes: 0 additions & 15 deletions x-pack/plugins/transform/public/app/hooks/use_request.ts

This file was deleted.

Expand Up @@ -6,16 +6,20 @@
*/

import React, { createContext } from 'react';
import { useQuery } from '@tanstack/react-query';

import { Privileges } from '../../../../../common/types/privileges';
import type { IHttpFetchError } from '@kbn/core-http-browser';

import { useRequest } from '../../../hooks';
import type { Privileges } from '../../../../../common/types/privileges';

import {
TransformCapabilities,
type PrivilegesAndCapabilities,
type TransformCapabilities,
INITIAL_CAPABILITIES,
} from '../../../../../common/privilege/has_privilege_factory';

import { useAppDependencies } from '../../../app_dependencies';

interface Authorization {
isLoading: boolean;
apiError: Error | null;
Expand All @@ -41,22 +45,36 @@ interface Props {
}

export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) => {
const { http } = useAppDependencies();

const { path, version } = privilegesEndpoint;

const {
isLoading,
error,
data: privilegesData,
} = useRequest({
path,
version,
method: 'get',
});
} = useQuery<PrivilegesAndCapabilities, IHttpFetchError>(
['transform-privileges-and-capabilities'],
async ({ signal }) => {
return await http.fetch<PrivilegesAndCapabilities>(path, {
version,
method: 'GET',
signal,
});
}
);

const value = {
isLoading,
privileges: isLoading ? { ...initialValue.privileges } : privilegesData.privileges,
capabilities: isLoading ? { ...INITIAL_CAPABILITIES } : privilegesData.capabilities,
apiError: error ? (error as Error) : null,
privileges:
isLoading || privilegesData === undefined
? { ...initialValue.privileges }
: privilegesData.privileges,
capabilities:
isLoading || privilegesData === undefined
? { ...INITIAL_CAPABILITIES }
: privilegesData.capabilities,
apiError: error ? error : null,
};

return (
Expand Down
2 changes: 0 additions & 2 deletions x-pack/plugins/transform/public/shared_imports.ts
Expand Up @@ -6,8 +6,6 @@
*/

export { XJsonMode } from '@kbn/ace';
export type { UseRequestConfig } from '@kbn/es-ui-shared-plugin/public';
export { useRequest } from '@kbn/es-ui-shared-plugin/public';

export type { GetMlSharedImportsReturnType } from '@kbn/ml-plugin/public';
export { getMlSharedImports } from '@kbn/ml-plugin/public';
Expand Down
100 changes: 49 additions & 51 deletions x-pack/plugins/transform/server/routes/api/privileges.ts
Expand Up @@ -5,15 +5,17 @@
* 2.0.
*/

import type { IScopedClusterClient } from '@kbn/core/server';
import type { IKibanaResponse, IScopedClusterClient } from '@kbn/core/server';
import type { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types';
import { getPrivilegesAndCapabilities } from '../../../common/privilege/has_privilege_factory';
import {
getPrivilegesAndCapabilities,
type PrivilegesAndCapabilities,
} from '../../../common/privilege/has_privilege_factory';
import {
addInternalBasePath,
APP_CLUSTER_PRIVILEGES,
APP_INDEX_PRIVILEGES,
} from '../../../common/constants';
import type { Privileges } from '../../../common/types/privileges';

import type { RouteDependencies } from '../../types';

Expand All @@ -23,64 +25,60 @@ export function registerPrivilegesRoute({ router, license }: RouteDependencies)
path: addInternalBasePath('privileges'),
access: 'internal',
})
.addVersion(
.addVersion<undefined, undefined, undefined>(
{
version: '1',
validate: false,
},
license.guardApiRoute(async (ctx, req, res) => {
const privilegesResult: Privileges = {
hasAllPrivileges: true,
missingPrivileges: {
cluster: [],
index: [],
},
};

if (license.getStatus().isSecurityEnabled === false) {
// If security isn't enabled, let the user use app.
return res.ok({ body: privilegesResult });
}
license.guardApiRoute<undefined, undefined, undefined>(
async (ctx, req, res): Promise<IKibanaResponse<PrivilegesAndCapabilities>> => {
if (license.getStatus().isSecurityEnabled === false) {
// If security isn't enabled, let the user use app.
return res.ok({
body: getPrivilegesAndCapabilities({}, true, true),
});
}

const esClient: IScopedClusterClient = (await ctx.core).elasticsearch.client;
const esClient: IScopedClusterClient = (await ctx.core).elasticsearch.client;

const esClusterPrivilegesReq: Promise<SecurityHasPrivilegesResponse> =
esClient.asCurrentUser.security.hasPrivileges({
body: {
cluster: APP_CLUSTER_PRIVILEGES,
},
});
const [esClusterPrivileges, userPrivileges] = await Promise.all([
// Get cluster privileges
esClusterPrivilegesReq,
// // Get all index privileges the user has
esClient.asCurrentUser.security.getUserPrivileges(),
]);
const esClusterPrivilegesReq: Promise<SecurityHasPrivilegesResponse> =
esClient.asCurrentUser.security.hasPrivileges({
body: {
cluster: APP_CLUSTER_PRIVILEGES,
},
});
const [esClusterPrivileges, userPrivileges] = await Promise.all([
// Get cluster privileges
esClusterPrivilegesReq,
// // Get all index privileges the user has
esClient.asCurrentUser.security.getUserPrivileges(),
]);

const { has_all_requested: hasAllPrivileges, cluster } = esClusterPrivileges;
const { indices } = userPrivileges;
const { has_all_requested: hasAllPrivileges, cluster } = esClusterPrivileges;
const { indices } = userPrivileges;

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

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

return indexHasAllPrivileges;
}) !== undefined;
return indexHasAllPrivileges;
}) !== undefined;

return res.ok({
body: getPrivilegesAndCapabilities(
cluster,
hasOneIndexWithAllPrivileges,
hasAllPrivileges
),
});
})
return res.ok({
body: getPrivilegesAndCapabilities(
cluster,
hasOneIndexWithAllPrivileges,
hasAllPrivileges
),
});
}
)
);
}

0 comments on commit 3be6115

Please sign in to comment.