[8.16] [Spaces and Roles] Fix rules on showing Permissions tab (#196442) (#196938)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[Spaces and Roles] Fix rules on showing `Permissions` tab
(#196442)](https://github.com/elastic/kibana/pull/196442)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Tim
Sullivan","email":"tsullivan@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-18T15:39:57Z","message":"[Spaces
and Roles] Fix rules on showing `Permissions` tab (#196442)\n\n##
Summary\r\n\r\nSome deployment types don't have custom roles. This PR
hides the\r\n`Permissions` tab in Spaces Management for projects without
role\r\nmanagement enabled. The solution uses the
isRoleManagementEnabled\r\nfunction from the security plugin’s authz
service for this.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"4ed9e5173c6d56a949c15fcb8aa223cfc64fc5f1","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Security","release_note:skip","v9.0.0","v8.16.0","backport:version","v8.17.0"],"number":196442,"url":"https://github.com/elastic/kibana/pull/196442","mergeCommit":{"message":"[Spaces
and Roles] Fix rules on showing `Permissions` tab (#196442)\n\n##
Summary\r\n\r\nSome deployment types don't have custom roles. This PR
hides the\r\n`Permissions` tab in Spaces Management for projects without
role\r\nmanagement enabled. The solution uses the
isRoleManagementEnabled\r\nfunction from the security plugin’s authz
service for this.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"4ed9e5173c6d56a949c15fcb8aa223cfc64fc5f1"}},"sourceBranch":"main","suggestedTargetBranches":["8.16","8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196442","number":196442,"mergeCommit":{"message":"[Spaces
and Roles] Fix rules on showing `Permissions` tab (#196442)\n\n##
Summary\r\n\r\nSome deployment types don't have custom roles. This PR
hides the\r\n`Permissions` tab in Spaces Management for projects without
role\r\nmanagement enabled. The solution uses the
isRoleManagementEnabled\r\nfunction from the security plugin’s authz
service for this.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"4ed9e5173c6d56a949c15fcb8aa223cfc64fc5f1"}},{"branch":"8.16","label":"v8.16.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.x","label":"v8.17.0","labelRegex":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Tim Sullivan 2024-10-21 13:48:28 -07:00 committed by GitHub
parent cce59545c4
commit 49593afcbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 233 additions and 15 deletions

View file

@ -68,8 +68,14 @@ export const EditSpace: FC<PageProps> = ({
}) => {
const { state, dispatch } = useEditSpaceStore();
const { invokeClient } = useEditSpaceServices();
const { spacesManager, capabilities, serverBasePath, logger, notifications } =
useEditSpaceServices();
const {
spacesManager,
capabilities,
serverBasePath,
logger,
notifications,
isRoleManagementEnabled,
} = useEditSpaceServices();
const [space, setSpace] = useState<Space | null>(null);
const [userActiveSpace, setUserActiveSpace] = useState<Space | null>(null);
const [features, setFeatures] = useState<KibanaFeature[] | null>(null);
@ -80,6 +86,7 @@ export const EditSpace: FC<PageProps> = ({
const [tabs, selectedTabContent] = useTabs({
space,
features,
isRoleManagementEnabled,
rolesCount: state.roles.size,
capabilities,
history,
@ -139,10 +146,18 @@ export const EditSpace: FC<PageProps> = ({
setIsLoadingRoles(false);
};
if (!state.roles.size && !state.fetchRolesError) {
if (isRoleManagementEnabled && !state.roles.size && !state.fetchRolesError) {
getRoles();
}
}, [dispatch, invokeClient, spaceId, state.roles, state.fetchRolesError, logger]);
}, [
dispatch,
invokeClient,
spaceId,
logger,
state.roles,
state.fetchRolesError,
isRoleManagementEnabled,
]);
useEffect(() => {
const _getFeatures = async () => {
@ -165,7 +180,7 @@ export const EditSpace: FC<PageProps> = ({
return null;
}
if (isLoadingSpace || isLoadingFeatures || isLoadingRoles) {
if (isLoadingSpace || isLoadingFeatures || (isRoleManagementEnabled && isLoadingRoles)) {
return (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>

View file

@ -58,6 +58,7 @@ const TestComponent: React.FC<React.PropsWithChildren> = ({ children }) => {
http={http}
notifications={notifications}
overlays={overlays}
getIsRoleManagementEnabled={() => Promise.resolve(() => undefined)}
getPrivilegesAPIClient={getPrivilegeAPIClient}
getSecurityLicense={getSecurityLicenseMock}
theme={theme}

View file

@ -80,6 +80,7 @@ describe('EditSpaceSettings', () => {
http={http}
notifications={notifications}
overlays={overlays}
getIsRoleManagementEnabled={() => Promise.resolve(() => undefined)}
getPrivilegesAPIClient={getPrivilegeAPIClient}
getSecurityLicense={getSecurityLicenseMock}
theme={theme}

View file

@ -49,7 +49,14 @@ describe('EditSpaceAssignedRolesTab', () => {
const loadRolesSpy = jest.spyOn(spacesManager, 'getRolesForSpace');
const toastErrorSpy = jest.spyOn(notifications.toasts, 'addError');
const TestComponent: React.FC<React.PropsWithChildren> = ({ children }) => {
const TestComponent: React.FC<
React.PropsWithChildren<{
getIsRoleManagementEnabled?: () => Promise<() => boolean | undefined>;
}>
> = ({ children, ...props }) => {
const getIsRoleManagementEnabled =
props.getIsRoleManagementEnabled ?? (() => Promise.resolve(() => undefined));
return (
<IntlProvider locale="en">
<EditSpaceProviderRoot
@ -67,6 +74,7 @@ describe('EditSpaceAssignedRolesTab', () => {
http={http}
notifications={notifications}
overlays={overlays}
getIsRoleManagementEnabled={getIsRoleManagementEnabled}
getPrivilegesAPIClient={getPrivilegeAPIClient}
getSecurityLicense={getSecurityLicenseMock}
theme={theme}
@ -118,4 +126,21 @@ describe('EditSpaceAssignedRolesTab', () => {
});
});
});
it('does not load roles if role management is not enabled', async () => {
const getIsRoleManagementEnabled = () => Promise.resolve(() => false);
act(() => {
render(
<TestComponent getIsRoleManagementEnabled={getIsRoleManagementEnabled}>
<EditSpaceAssignedRolesTab space={space} isReadOnly={false} features={[]} />
</TestComponent>
);
});
await waitFor(() => {
expect(loadRolesSpy).not.toHaveBeenCalled();
expect(toastErrorSpy).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,123 @@
/*
* 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 { render } from '@testing-library/react';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import { scopedHistoryMock } from '@kbn/core-application-browser-mocks';
import { KibanaFeature } from '@kbn/features-plugin/common';
import type { GetTabsProps } from './edit_space_tabs';
import { getTabs } from './edit_space_tabs';
const space = {
id: 'my-space',
name: 'My Space',
disabledFeatures: [],
};
const features = [
new KibanaFeature({
id: 'feature-1',
name: 'feature 1',
app: [],
category: DEFAULT_APP_CATEGORIES.kibana,
privileges: null,
}),
];
const history = scopedHistoryMock.create();
const allowFeatureVisibility = true;
const allowSolutionVisibility = true;
const getCapabilities = (
options: Partial<GetTabsProps['capabilities']> = { roles: { save: false, view: true } }
) => ({
navLinks: {},
management: {},
catalogue: {},
spaces: { manage: true },
...options,
});
describe('Edit Space Tabs: getTabs', () => {
it('can include a Permissions tab', () => {
const isRoleManagementEnabled = true;
const capabilities = getCapabilities();
expect(
getTabs({
isRoleManagementEnabled,
capabilities,
space,
features,
history,
allowFeatureVisibility,
allowSolutionVisibility,
}).map(({ id, name }) => ({ name, id }))
).toEqual([
{ id: 'general', name: 'General settings' },
{ id: 'roles', name: 'Permissions' },
{ id: 'content', name: 'Content' },
]);
});
it('can include count of roles as a badge for Permissions tab', () => {
const isRoleManagementEnabled = true;
const capabilities = getCapabilities();
const rolesTab = getTabs({
rolesCount: 42,
isRoleManagementEnabled,
capabilities,
space,
features,
history,
allowFeatureVisibility,
allowSolutionVisibility,
}).find((tab) => tab.id === 'roles');
if (!rolesTab?.append) {
throw new Error('roles tab did not exist or did not have a badge!');
}
const { getByText } = render(rolesTab.append);
expect(getByText('42')).toBeInTheDocument();
});
it('hides Permissions tab when role management is not enabled', () => {
expect(
getTabs({
space,
isRoleManagementEnabled: false,
capabilities: getCapabilities(),
features,
history,
allowFeatureVisibility,
allowSolutionVisibility,
}).map(({ id, name }) => ({ name, id }))
).toEqual([
{ id: 'general', name: 'General settings' },
{ id: 'content', name: 'Content' },
]);
});
it('hides Permissions tab when role capabilities do not include "view"', () => {
expect(
getTabs({
space,
isRoleManagementEnabled: true,
capabilities: getCapabilities({ roles: { save: false, view: false } }),
features,
history,
allowFeatureVisibility,
allowSolutionVisibility,
}).map(({ id, name }) => ({ name, id }))
).toEqual([
{ id: 'general', name: 'General settings' },
{ id: 'content', name: 'Content' },
]);
});
});

View file

@ -26,7 +26,8 @@ export interface EditSpaceTab {
export interface GetTabsProps {
space: Space;
rolesCount: number;
rolesCount?: number;
isRoleManagementEnabled: boolean;
features: KibanaFeature[];
history: ScopedHistory;
capabilities: Capabilities & {
@ -66,10 +67,9 @@ export const getTabs = ({
history,
capabilities,
rolesCount,
isRoleManagementEnabled,
...props
}: GetTabsProps): EditSpaceTab[] => {
const canUserViewRoles = Boolean(capabilities?.roles?.view);
const canUserModifyRoles = Boolean(capabilities?.roles?.save);
const reloadWindow = () => {
window.location.reload();
};
@ -92,7 +92,9 @@ export const getTabs = ({
},
];
if (canUserViewRoles) {
const canUserViewRoles = Boolean(capabilities?.roles?.view);
const canUserModifyRoles = Boolean(capabilities?.roles?.save);
if (canUserViewRoles && isRoleManagementEnabled) {
tabsDefinition.push({
id: TAB_ID_ROLES,
name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.roles.heading', {
@ -100,7 +102,7 @@ export const getTabs = ({
}),
append: (
<EuiNotificationBadge className="eui-alignCenter" color="subdued" size="m">
{rolesCount}
{rolesCount ?? 0}
</EuiNotificationBadge>
),
content: (

View file

@ -17,6 +17,7 @@ type UseTabsProps = Pick<GetTabsProps, 'capabilities' | 'rolesCount'> & {
space: Space | null;
features: KibanaFeature[] | null;
currentSelectedTabId: string;
isRoleManagementEnabled: boolean;
history: ScopedHistory;
allowFeatureVisibility: boolean;
allowSolutionVisibility: boolean;

View file

@ -61,6 +61,7 @@ const SUTProvider = ({
spacesManager,
serverBasePath: '',
getUrlForApp: (_) => _,
getIsRoleManagementEnabled: () => Promise.resolve(() => undefined),
getRolesAPIClient: getRolesAPIClientMock,
getPrivilegesAPIClient: getPrivilegeAPIClientMock,
getSecurityLicense: getSecurityLicenseMock,

View file

@ -15,6 +15,7 @@ import React, {
useEffect,
useReducer,
useRef,
useState,
} from 'react';
import type { ApplicationStart } from '@kbn/core-application-browser';
@ -41,6 +42,7 @@ export interface EditSpaceProviderRootProps
navigateToUrl: ApplicationStart['navigateToUrl'];
serverBasePath: string;
spacesManager: SpacesManager;
getIsRoleManagementEnabled: () => Promise<() => boolean | undefined>;
getRolesAPIClient: () => Promise<RolesAPIClient>;
getPrivilegesAPIClient: () => Promise<PrivilegesAPIClientPublicContract>;
getSecurityLicense: () => Promise<SecurityLicense>;
@ -55,10 +57,14 @@ interface EditSpaceClients {
export interface EditSpaceServices
extends Omit<
EditSpaceProviderRootProps,
'getRolesAPIClient' | 'getPrivilegesAPIClient' | 'getSecurityLicense'
| 'getRolesAPIClient'
| 'getPrivilegesAPIClient'
| 'getSecurityLicense'
| 'getIsRoleManagementEnabled'
> {
invokeClient<R extends unknown>(arg: (clients: EditSpaceClients) => Promise<R>): Promise<R>;
license?: SecurityLicense;
isRoleManagementEnabled: boolean;
}
export interface EditSpaceStore {
@ -101,8 +107,15 @@ export const EditSpaceProviderRoot = ({
children,
...services
}: PropsWithChildren<EditSpaceProviderRootProps>) => {
const { logger, getRolesAPIClient, getPrivilegesAPIClient, getSecurityLicense } = services;
const {
logger,
getRolesAPIClient,
getPrivilegesAPIClient,
getSecurityLicense,
getIsRoleManagementEnabled,
} = services;
const [isRoleManagementEnabled, setIsRoleManagementEnabled] = useState<boolean>(false);
const clients = useRef(Promise.all([getRolesAPIClient(), getPrivilegesAPIClient()]));
const license = useRef(getSecurityLicense);
@ -162,9 +175,21 @@ export const EditSpaceProviderRoot = ({
[resolveAPIClients, services.spacesManager]
);
getIsRoleManagementEnabled().then((isEnabledFunction) => {
const result = isEnabledFunction();
setIsRoleManagementEnabled(typeof result === 'undefined' || result);
});
return (
<EditSpaceProvider
{...{ ...services, invokeClient, state, dispatch, license: licenseRef.current }}
{...{
...services,
invokeClient,
state,
dispatch,
license: licenseRef.current,
isRoleManagementEnabled,
}}
>
{children}
</EditSpaceProvider>

View file

@ -95,6 +95,7 @@ const renderPrivilegeRolesForm = ({
getUrlForApp: jest.fn((_) => _),
navigateToUrl: jest.fn(),
license: licenseMock,
isRoleManagementEnabled: true,
capabilities: {
navLinks: {},
management: {},

View file

@ -48,6 +48,7 @@ describe('ManagementService', () => {
spacesManager: spacesManagerMock.create(),
config,
logger,
getIsRoleManagementEnabled: () => Promise.resolve(() => undefined),
getRolesAPIClient: getRolesAPIClientMock,
getPrivilegesAPIClient: jest.fn(),
getSecurityLicense: getSecurityLicenseMock,
@ -72,6 +73,7 @@ describe('ManagementService', () => {
spacesManager: spacesManagerMock.create(),
config,
logger,
getIsRoleManagementEnabled: () => Promise.resolve(() => undefined),
getRolesAPIClient: getRolesAPIClientMock,
getPrivilegesAPIClient: jest.fn(),
getSecurityLicense: getSecurityLicenseMock,
@ -97,6 +99,7 @@ describe('ManagementService', () => {
spacesManager: spacesManagerMock.create(),
config,
logger,
getIsRoleManagementEnabled: () => Promise.resolve(() => undefined),
getRolesAPIClient: jest.fn(),
getPrivilegesAPIClient: jest.fn(),
getSecurityLicense: getSecurityLicenseMock,

View file

@ -25,6 +25,7 @@ export class ManagementService {
spacesManager,
config,
logger,
getIsRoleManagementEnabled,
getRolesAPIClient,
eventTracker,
getPrivilegesAPIClient,
@ -36,6 +37,7 @@ export class ManagementService {
spacesManager,
config,
logger,
getIsRoleManagementEnabled,
getRolesAPIClient,
eventTracker,
getPrivilegesAPIClient,

View file

@ -75,6 +75,7 @@ async function mountApp(basePath: string, pathname: string, spaceId?: string) {
getStartServices: async () => [coreStart, pluginsStart as PluginsStart, {}],
config,
logger,
getIsRoleManagementEnabled: () => Promise.resolve(() => undefined),
getRolesAPIClient: jest.fn(),
getPrivilegesAPIClient: jest.fn(),
getSecurityLicense: jest.fn(),
@ -100,6 +101,7 @@ describe('spacesManagementApp', () => {
getStartServices: coreMock.createSetup().getStartServices as any,
config,
logger,
getIsRoleManagementEnabled: () => Promise.resolve(() => undefined),
getRolesAPIClient: jest.fn(),
getPrivilegesAPIClient: jest.fn(),
getSecurityLicense: jest.fn(),

View file

@ -34,6 +34,7 @@ export interface CreateParams {
spacesManager: SpacesManager;
config: ConfigType;
logger: Logger;
getIsRoleManagementEnabled: () => Promise<() => boolean | undefined>;
getRolesAPIClient: () => Promise<RolesAPIClient>;
eventTracker: EventTracker;
getPrivilegesAPIClient: () => Promise<PrivilegesAPIClientPublicContract>;
@ -48,6 +49,7 @@ export const spacesManagementApp = Object.freeze({
config,
logger,
eventTracker,
getIsRoleManagementEnabled,
getRolesAPIClient,
getPrivilegesAPIClient,
getSecurityLicense,
@ -163,6 +165,7 @@ export const spacesManagementApp = Object.freeze({
onLoadSpace={onLoadSpace}
history={history}
selectedTabId={selectedTabId}
getIsRoleManagementEnabled={getIsRoleManagementEnabled}
getRolesAPIClient={getRolesAPIClient}
allowFeatureVisibility={config.allowFeatureVisibility}
allowSolutionVisibility={config.allowSolutionVisibility}

View file

@ -90,6 +90,17 @@ export class SpacesPlugin implements Plugin<SpacesPluginSetup, SpacesPluginStart
// Only skip setup of space selector and management service if serverless and only one space is allowed
if (!(this.isServerless && hasOnlyDefaultSpace)) {
const getIsRoleManagementEnabled = async () => {
const { security } = await core.plugins.onSetup<{ security: SecurityPluginStart }>(
'security'
);
if (!security.found) {
throw new Error('Security plugin is not available as runtime dependency.');
}
return security.contract.authz.isRoleManagementEnabled;
};
const getRolesAPIClient = async () => {
const { security } = await core.plugins.onSetup<{ security: SecurityPluginStart }>(
'security'
@ -138,6 +149,7 @@ export class SpacesPlugin implements Plugin<SpacesPluginSetup, SpacesPluginStart
spacesManager: this.spacesManager,
config: this.config,
logger: this.initializerContext.logger.get(),
getIsRoleManagementEnabled,
getRolesAPIClient,
eventTracker: this.eventTracker,
getPrivilegesAPIClient,

View file

@ -50,7 +50,8 @@
"@kbn/core-notifications-browser",
"@kbn/logging",
"@kbn/core-logging-browser-mocks",
"@kbn/core-http-router-server-mocks"
"@kbn/core-http-router-server-mocks",
"@kbn/core-application-browser-mocks"
],
"exclude": [
"target/**/*",