mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Component templates] Privileges support (#68733)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
ccf8def829
commit
1a933c293d
15 changed files with 395 additions and 4 deletions
|
@ -9,6 +9,7 @@
|
|||
"management"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"security",
|
||||
"usageCollection"
|
||||
],
|
||||
"configPath": ["xpack", "index_management"]
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
nextTick,
|
||||
} from '../../../../../../../../../test_utils';
|
||||
import { WithAppDependencies } from './setup_environment';
|
||||
import { ComponentTemplateList } from '../../../component_template_list';
|
||||
import { ComponentTemplateList } from '../../../component_template_list/component_template_list';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { AuthorizationProvider } from '../shared_imports';
|
||||
import { useComponentTemplatesContext } from '../component_templates_context';
|
||||
|
||||
export const ComponentTemplatesAuthProvider: React.FunctionComponent = ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const { httpClient, apiBasePath } = useComponentTemplatesContext();
|
||||
|
||||
return (
|
||||
<AuthorizationProvider
|
||||
privilegesEndpoint={`${apiBasePath}/component_templates/privileges`}
|
||||
httpClient={httpClient}
|
||||
>
|
||||
{children}
|
||||
</AuthorizationProvider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { ComponentTemplatesAuthProvider } from './auth_provider';
|
||||
import { ComponentTemplatesWithPrivileges } from './with_privileges';
|
||||
import { ComponentTemplateList } from './component_template_list';
|
||||
|
||||
export const ComponentTemplateListContainer: React.FunctionComponent = () => {
|
||||
return (
|
||||
<ComponentTemplatesAuthProvider>
|
||||
<ComponentTemplatesWithPrivileges>
|
||||
<ComponentTemplateList />
|
||||
</ComponentTemplatesWithPrivileges>
|
||||
</ComponentTemplatesAuthProvider>
|
||||
);
|
||||
};
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ComponentTemplateList } from './component_template_list';
|
||||
export { ComponentTemplateListContainer as ComponentTemplateList } from './component_template_list_container';
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
import {
|
||||
SectionError,
|
||||
useAuthorizationContext,
|
||||
WithPrivileges,
|
||||
SectionLoading,
|
||||
NotAuthorizedSection,
|
||||
} from '../shared_imports';
|
||||
import { APP_CLUSTER_REQUIRED_PRIVILEGES } from '../constants';
|
||||
|
||||
export const ComponentTemplatesWithPrivileges: FunctionComponent = ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const { apiError } = useAuthorizationContext();
|
||||
|
||||
if (apiError) {
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.home.componentTemplates.checkingPrivilegesErrorMessage"
|
||||
defaultMessage="Error fetching user privileges from the server."
|
||||
/>
|
||||
}
|
||||
error={apiError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WithPrivileges
|
||||
privileges={APP_CLUSTER_REQUIRED_PRIVILEGES.map((privilege) => `cluster.${privilege}`)}
|
||||
>
|
||||
{({ isLoading, hasPrivileges, privilegesMissing }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.home.componentTemplates.checkingPrivilegesDescription"
|
||||
defaultMessage="Checking privileges…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasPrivileges) {
|
||||
return (
|
||||
<NotAuthorizedSection
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.home.componentTemplates.deniedPrivilegeTitle"
|
||||
defaultMessage="Cluster privileges required"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.home.componentTemplates.deniedPrivilegeDescription"
|
||||
defaultMessage="To use Component Templates, you must have {privilegesCount,
|
||||
plural, one {this cluster privilege} other {these cluster privileges}}: {missingPrivileges}."
|
||||
values={{
|
||||
missingPrivileges: privilegesMissing.cluster!.join(', '),
|
||||
privilegesCount: privilegesMissing.cluster!.length,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}}
|
||||
</WithPrivileges>
|
||||
);
|
||||
};
|
|
@ -21,6 +21,8 @@ interface Props {
|
|||
}
|
||||
|
||||
interface Context {
|
||||
httpClient: HttpSetup;
|
||||
apiBasePath: string;
|
||||
api: ReturnType<typeof getApi>;
|
||||
documentation: ReturnType<typeof getDocumentation>;
|
||||
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
|
||||
|
@ -45,7 +47,7 @@ export const ComponentTemplatesProvider = ({
|
|||
|
||||
return (
|
||||
<ComponentTemplatesContext.Provider
|
||||
value={{ api, documentation, trackMetric, toasts, appBasePath }}
|
||||
value={{ api, documentation, trackMetric, toasts, appBasePath, httpClient, apiBasePath }}
|
||||
>
|
||||
{children}
|
||||
</ComponentTemplatesContext.Provider>
|
||||
|
|
|
@ -8,3 +8,6 @@
|
|||
export const UIM_COMPONENT_TEMPLATE_LIST_LOAD = 'component_template_list_load';
|
||||
export const UIM_COMPONENT_TEMPLATE_DELETE = 'component_template_delete';
|
||||
export const UIM_COMPONENT_TEMPLATE_DELETE_MANY = 'component_template_delete_many';
|
||||
|
||||
// privileges
|
||||
export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_index_templates'];
|
||||
|
|
|
@ -12,4 +12,10 @@ export {
|
|||
sendRequest,
|
||||
useRequest,
|
||||
SectionLoading,
|
||||
WithPrivileges,
|
||||
AuthorizationProvider,
|
||||
SectionError,
|
||||
Error,
|
||||
useAuthorizationContext,
|
||||
NotAuthorizedSection,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/public';
|
||||
|
|
|
@ -59,7 +59,7 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
|
|||
|
||||
setup(
|
||||
{ http, getStartServices }: CoreSetup,
|
||||
{ licensing }: Dependencies
|
||||
{ licensing, security }: Dependencies
|
||||
): IndexManagementPluginSetup {
|
||||
const router = http.createRouter();
|
||||
|
||||
|
@ -89,6 +89,9 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
|
|||
this.apiRoutes.setup({
|
||||
router,
|
||||
license: this.license,
|
||||
config: {
|
||||
isSecurityEnabled: () => security !== undefined && security.license.isEnabled(),
|
||||
},
|
||||
indexDataEnricher: this.indexDataEnricher,
|
||||
lib: {
|
||||
isEsError,
|
||||
|
|
|
@ -10,10 +10,12 @@ import { registerGetAllRoute } from './get';
|
|||
import { registerCreateRoute } from './create';
|
||||
import { registerUpdateRoute } from './update';
|
||||
import { registerDeleteRoute } from './delete';
|
||||
import { registerPrivilegesRoute } from './privileges';
|
||||
|
||||
export function registerComponentTemplateRoutes(dependencies: RouteDependencies) {
|
||||
registerGetAllRoute(dependencies);
|
||||
registerCreateRoute(dependencies);
|
||||
registerUpdateRoute(dependencies);
|
||||
registerDeleteRoute(dependencies);
|
||||
registerPrivilegesRoute(dependencies);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 { httpServerMock, httpServiceMock } from 'src/core/server/mocks';
|
||||
import {
|
||||
kibanaResponseFactory,
|
||||
RequestHandlerContext,
|
||||
RequestHandler,
|
||||
IRouter,
|
||||
} from 'src/core/server';
|
||||
|
||||
import { License } from '../../../services/license';
|
||||
import { IndexDataEnricher } from '../../../services/index_data_enricher';
|
||||
|
||||
import { registerPrivilegesRoute } from './privileges';
|
||||
|
||||
jest.mock('../../../services/index_data_enricher');
|
||||
|
||||
const httpService = httpServiceMock.createSetupContract();
|
||||
|
||||
const mockedIndexDataEnricher = new IndexDataEnricher();
|
||||
|
||||
const mockRouteContext = ({
|
||||
callAsCurrentUser,
|
||||
}: {
|
||||
callAsCurrentUser: any;
|
||||
}): RequestHandlerContext => {
|
||||
const routeContextMock = ({
|
||||
core: {
|
||||
elasticsearch: {
|
||||
legacy: {
|
||||
client: {
|
||||
callAsCurrentUser,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown) as RequestHandlerContext;
|
||||
|
||||
return routeContextMock;
|
||||
};
|
||||
|
||||
describe('GET privileges', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
|
||||
beforeEach(() => {
|
||||
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
|
||||
|
||||
registerPrivilegesRoute({
|
||||
router,
|
||||
license: {
|
||||
guardApiRoute: (route: any) => route,
|
||||
} as License,
|
||||
config: {
|
||||
isSecurityEnabled: () => true,
|
||||
},
|
||||
indexDataEnricher: mockedIndexDataEnricher,
|
||||
lib: {
|
||||
isEsError: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
routeHandler = router.get.mock.calls[0][1];
|
||||
});
|
||||
|
||||
it('should return the correct response when a user has privileges', async () => {
|
||||
const privilegesResponseMock = {
|
||||
username: 'elastic',
|
||||
has_all_requested: true,
|
||||
cluster: { manage_index_templates: true },
|
||||
index: {},
|
||||
application: {},
|
||||
};
|
||||
|
||||
const routeContextMock = mockRouteContext({
|
||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce(privilegesResponseMock),
|
||||
});
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
|
||||
|
||||
expect(response.payload).toEqual({
|
||||
hasAllPrivileges: true,
|
||||
missingPrivileges: {
|
||||
cluster: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct response when a user does not have privileges', async () => {
|
||||
const privilegesResponseMock = {
|
||||
username: 'elastic',
|
||||
has_all_requested: false,
|
||||
cluster: { manage_index_templates: false },
|
||||
index: {},
|
||||
application: {},
|
||||
};
|
||||
|
||||
const routeContextMock = mockRouteContext({
|
||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce(privilegesResponseMock),
|
||||
});
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
|
||||
|
||||
expect(response.payload).toEqual({
|
||||
hasAllPrivileges: false,
|
||||
missingPrivileges: {
|
||||
cluster: ['manage_index_templates'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('With security disabled', () => {
|
||||
beforeEach(() => {
|
||||
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
|
||||
|
||||
registerPrivilegesRoute({
|
||||
router,
|
||||
license: {
|
||||
guardApiRoute: (route: any) => route,
|
||||
} as License,
|
||||
config: {
|
||||
isSecurityEnabled: () => false,
|
||||
},
|
||||
indexDataEnricher: mockedIndexDataEnricher,
|
||||
lib: {
|
||||
isEsError: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
routeHandler = router.get.mock.calls[0][1];
|
||||
});
|
||||
|
||||
it('should return the default privileges response', async () => {
|
||||
const routeContextMock = mockRouteContext({
|
||||
callAsCurrentUser: jest.fn(),
|
||||
});
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
|
||||
|
||||
expect(response.payload).toEqual({
|
||||
hasAllPrivileges: true,
|
||||
missingPrivileges: {
|
||||
cluster: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { Privileges } from 'src/plugins/es_ui_shared/public';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
import { addBasePath } from '../index';
|
||||
|
||||
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 registerPrivilegesRoute = ({ license, router, config }: RouteDependencies) => {
|
||||
router.get(
|
||||
{
|
||||
path: addBasePath('/component_templates/privileges'),
|
||||
validate: false,
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const privilegesResult: Privileges = {
|
||||
hasAllPrivileges: true,
|
||||
missingPrivileges: {
|
||||
cluster: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Skip the privileges check if security is not enabled
|
||||
if (!config.isSecurityEnabled()) {
|
||||
return res.ok({ body: privilegesResult });
|
||||
}
|
||||
|
||||
const {
|
||||
core: {
|
||||
elasticsearch: {
|
||||
legacy: { client },
|
||||
},
|
||||
},
|
||||
} = ctx;
|
||||
|
||||
try {
|
||||
const { has_all_requested: hasAllPrivileges, cluster } = await client.callAsCurrentUser(
|
||||
'transport.request',
|
||||
{
|
||||
path: '/_security/user/_has_privileges',
|
||||
method: 'POST',
|
||||
body: {
|
||||
cluster: ['manage_index_templates'],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!hasAllPrivileges) {
|
||||
privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster);
|
||||
}
|
||||
|
||||
privilegesResult.hasAllPrivileges = hasAllPrivileges;
|
||||
|
||||
return res.ok({ body: privilegesResult });
|
||||
} catch (e) {
|
||||
return res.internalError({ body: e });
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
|
@ -5,16 +5,21 @@
|
|||
*/
|
||||
import { ScopedClusterClient, IRouter } from 'src/core/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
import { License, IndexDataEnricher } from './services';
|
||||
import { isEsError } from './lib/is_es_error';
|
||||
|
||||
export interface Dependencies {
|
||||
security: SecurityPluginSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
}
|
||||
|
||||
export interface RouteDependencies {
|
||||
router: IRouter;
|
||||
license: License;
|
||||
config: {
|
||||
isSecurityEnabled: () => boolean;
|
||||
};
|
||||
indexDataEnricher: IndexDataEnricher;
|
||||
lib: {
|
||||
isEsError: typeof isEsError;
|
||||
|
|
|
@ -338,5 +338,20 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(body.errors[0].error.msg).to.contain('index_template_missing_exception');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Privileges', () => {
|
||||
it('should return privileges result', async () => {
|
||||
const uri = `${API_BASE_PATH}/component_templates/privileges`;
|
||||
|
||||
const { body } = await supertest.get(uri).set('kbn-xsrf', 'xxx').expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
hasAllPrivileges: true,
|
||||
missingPrivileges: {
|
||||
cluster: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue