Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SecuritySolutions] Update asset criticality upload page visibility and permissions #180771

Merged
merged 7 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { updateAppLinks } from '../../links';
import { mockGlobalState } from '../../mock';
import type { Capabilities } from '@kbn/core-capabilities-common';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';

const defaultAppLinks: AppLinkItems = [
{
Expand All @@ -29,13 +30,15 @@ const defaultAppLinks: AppLinkItems = [
];

const mockUpselling = new UpsellingService();
const mockUiSettingsClient = uiSettingsServiceMock.createStartContract();

describe('helpers', () => {
beforeAll(() => {
updateAppLinks(defaultAppLinks, {
capabilities: {} as unknown as Capabilities,
experimentalFeatures: mockGlobalState.app.enableExperimental,
upselling: mockUpselling,
uiSettingsClient: mockUiSettingsClient,
});
});
it('returns the search string', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../comm
import type { Capabilities } from '@kbn/core/types';
import { mockGlobalState, TestProviders } from '../mock';
import type { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types';
import type { AppLinkItems } from './types';
import type { AppLinkItems, LinkItem, LinksPermissions } from './types';
import { act, renderHook } from '@testing-library/react-hooks';
import {
useAppLinks,
Expand All @@ -18,11 +18,13 @@ import {
needsUrlState,
updateAppLinks,
useLinkExists,
isLinkUiSettingsAllowed,
} from './links';
import { createCapabilities } from './test_utils';
import { hasCapabilities } from '../lib/capabilities';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import React from 'react';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';

const defaultAppLinks: AppLinkItems = [
{
Expand Down Expand Up @@ -79,6 +81,8 @@ const mockLicense = {
hasAtLeast: licensePremiumMock,
} as unknown as ILicense;

const mockUiSettingsClient = uiSettingsServiceMock.createStartContract();

const renderUseAppLinks = () =>
renderHook<{}, AppLinkItems>(() => useAppLinks(), { wrapper: TestProviders });
const renderUseLinkExists = (id: SecurityPageName) =>
Expand All @@ -95,6 +99,7 @@ describe('Security links', () => {
experimentalFeatures: mockExperimentalDefaults,
license: mockLicense,
upselling: mockUpselling,
uiSettingsClient: mockUiSettingsClient,
});
});

Expand Down Expand Up @@ -174,6 +179,7 @@ describe('Security links', () => {
} as unknown as typeof mockExperimentalDefaults,
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
upselling: mockUpselling,
uiSettingsClient: mockUiSettingsClient,
}
);
await waitForNextUpdate();
Expand Down Expand Up @@ -240,6 +246,7 @@ describe('Security links', () => {
} as unknown as typeof mockExperimentalDefaults,
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
upselling,
uiSettingsClient: mockUiSettingsClient,
}
);
await waitForNextUpdate();
Expand Down Expand Up @@ -269,6 +276,7 @@ describe('Security links', () => {
experimentalFeatures: mockExperimentalDefaults,
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
upselling: mockUpselling,
uiSettingsClient: mockUiSettingsClient,
});
await waitForNextUpdate();
});
Expand Down Expand Up @@ -300,6 +308,7 @@ describe('Security links', () => {
experimentalFeatures: mockExperimentalDefaults,
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
upselling: mockUpselling,
uiSettingsClient: mockUiSettingsClient,
});
await waitForNextUpdate();
});
Expand Down Expand Up @@ -338,6 +347,7 @@ describe('Security links', () => {
experimentalFeatures: mockExperimentalDefaults,
license: mockLicense,
upselling: new UpsellingService(),
uiSettingsClient: mockUiSettingsClient,
}
);
await waitForNextUpdate();
Expand Down Expand Up @@ -369,6 +379,7 @@ describe('Security links', () => {
experimentalFeatures: mockExperimentalDefaults,
license: mockLicense,
upselling: mockUpselling,
uiSettingsClient: mockUiSettingsClient,
}
);
await waitForNextUpdate();
Expand Down Expand Up @@ -532,4 +543,74 @@ describe('Security links', () => {
).toBeFalsy();
});
});

describe('isLinkUiSettingsAllowed', () => {
const SETTING_KEY = 'test setting';
const mockedLink: LinkItem = {
id: SecurityPageName.entityAnalyticsAssetClassification,
title: 'test title',
path: '/test_path',
};

const mockedPermissions = {
uiSettingsClient: mockUiSettingsClient,
} as unknown as LinksPermissions;

afterEach(() => {
jest.clearAllMocks();
});

it('returns true when uiSettingRequired is not set', () => {
const link: LinkItem = {
...mockedLink,
uiSettingRequired: undefined,
};
expect(isLinkUiSettingsAllowed(link, mockedPermissions)).toBeTruthy();
machadoum marked this conversation as resolved.
Show resolved Hide resolved
expect(mockUiSettingsClient.get).not.toHaveBeenCalled();
});

it('returns true when uiSettingRequired is a string and the corresponding UI setting is true', () => {
mockUiSettingsClient.get = jest.fn().mockReturnValue(true);
const link: LinkItem = {
...mockedLink,
uiSettingRequired: SETTING_KEY,
};

expect(isLinkUiSettingsAllowed(link, mockedPermissions)).toBeTruthy();
expect(mockUiSettingsClient.get).toHaveBeenCalledWith(SETTING_KEY);
});

it('returns false when uiSettingRequired is a string and the corresponding UI setting is false', () => {
mockUiSettingsClient.get = jest.fn().mockReturnValue(false);
const link: LinkItem = {
...mockedLink,
uiSettingRequired: SETTING_KEY,
};

expect(isLinkUiSettingsAllowed(link, mockedPermissions)).toBeFalsy();
expect(mockUiSettingsClient.get).toHaveBeenCalledWith(SETTING_KEY);
});

it('returns true when uiSettingRequired is an object and the corresponding UI setting matches the value', () => {
const link: LinkItem = {
...mockedLink,
uiSettingRequired: { key: SETTING_KEY, value: 'any text' },
};
mockUiSettingsClient.get = jest.fn().mockReturnValue('any text');

expect(isLinkUiSettingsAllowed(link, mockedPermissions)).toBeTruthy();
expect(mockUiSettingsClient.get).toHaveBeenCalledWith(SETTING_KEY);
});

it('returns false when uiSettingRequired is an object and the corresponding UI setting does not match the value', () => {
const link: LinkItem = {
...mockedLink,
uiSettingRequired: { key: SETTING_KEY, value: 'any text' },
};
mockUiSettingsClient.get = jest.fn().mockReturnValue('different text');

expect(isLinkUiSettingsAllowed(link, mockedPermissions)).toBeFalsy();
expect(mockUiSettingsClient.get).toHaveBeenCalledWith(SETTING_KEY);
});
});
});
22 changes: 21 additions & 1 deletion x-pack/plugins/security_solution/public/common/links/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ const getNormalizedLink = (id: SecurityPageName): Readonly<NormalizedLink> | und

const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissions): LinkItem[] =>
appLinks.reduce<LinkItem[]>((acc, { links, ...appLinkWithoutSublinks }) => {
if (!isLinkExperimentalKeyAllowed(appLinkWithoutSublinks, linksPermissions)) {
if (
!isLinkExperimentalKeyAllowed(appLinkWithoutSublinks, linksPermissions) ||
!isLinkUiSettingsAllowed(appLinkWithoutSublinks, linksPermissions)
) {
return acc;
}

Expand All @@ -179,6 +182,23 @@ const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissi
return acc;
}, []);

export const isLinkUiSettingsAllowed = (link: LinkItem, { uiSettingsClient }: LinksPermissions) => {
if (!link.uiSettingRequired) {
return true;
}

if (typeof link.uiSettingRequired === 'string') {
return uiSettingsClient.get(link.uiSettingRequired) === true;
}

if (typeof link.uiSettingRequired === 'object') {
return uiSettingsClient.get(link.uiSettingRequired.key) === link.uiSettingRequired.value;
}

// unsupported uiSettingRequired type
return false;
};

const isLinkExperimentalKeyAllowed = (
link: LinkItem,
{ experimentalFeatures }: LinksPermissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { UpsellingService } from '@kbn/security-solution-upselling/service'
import type { AppDeepLinkLocations } from '@kbn/core-application-browser';
import type { Observable } from 'rxjs';
import type { SolutionSideNavItem as ClassicSolutionSideNavItem } from '@kbn/security-solution-side-nav';
import type { IUiSettingsClient } from '@kbn/core/public';
import type { ExperimentalFeatures } from '../../../common/experimental_features';
import type { RequiredCapabilities } from '../lib/capabilities';

Expand All @@ -37,6 +38,7 @@ export type SolutionSideNavItem = ClassicSolutionSideNavItem<SolutionPageName>;
export interface LinksPermissions {
capabilities: Capabilities;
experimentalFeatures: Readonly<ExperimentalFeatures>;
uiSettingsClient: IUiSettingsClient;
upselling: UpsellingService;
license?: ILicense;
}
Expand Down Expand Up @@ -154,6 +156,13 @@ export interface LinkItem {
* Locations where the link is visible in the UI
*/
visibleIn?: AppDeepLinkLocations[];

/**
* Required UI setting to enable a link.
* To enable a link when a boolean UiSetting is true, pass the key as a string.
* To enable a link when a specific value is set for a UiSetting, pass an object with key and value.
*/
uiSettingRequired?: string | { key: string; value: unknown };
}

export type AppLinkItems = Readonly<LinkItem[]>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { UpsellingService } from '@kbn/security-solution-upselling/service';
import { updateAppLinks } from '../../links';
import { appLinks } from '../../../app_links';
import { useShowTimeline } from './use_show_timeline';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';

const mockUseLocation = jest.fn().mockReturnValue({ pathname: '/overview' });
jest.mock('react-router-dom', () => {
Expand Down Expand Up @@ -53,6 +54,7 @@ jest.mock('../../lib/kibana', () => {
});

const mockUpselling = new UpsellingService();
const mockUiSettingsClient = uiSettingsServiceMock.createStartContract();

describe('use show timeline', () => {
beforeAll(() => {
Expand All @@ -70,6 +72,7 @@ describe('use show timeline', () => {
},
},
upselling: mockUpselling,
uiSettingsClient: mockUiSettingsClient,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { UseMutationResult, UseQueryResult } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
import type { SecurityAppError } from '@kbn/securitysolution-t-grid';
import type { CriticalityLevelWithUnassigned } from '../../../../common/entity_analytics/asset_criticality/types';
import { ENABLE_ASSET_CRITICALITY_SETTING } from '../../../../common/constants';
import { useHasSecurityCapability } from '../../../helper_hooks';
Expand All @@ -21,21 +22,23 @@ const PRIVILEGES_KEY = 'PRIVILEGES';

const nonAuthorizedResponse: Promise<EntityAnalyticsPrivileges> = Promise.resolve({
has_all_required: false,
has_write_permissions: false,
has_read_permissions: false,
privileges: {
elasticsearch: {},
},
});

export const useAssetCriticalityPrivileges = (
entityName: string
): UseQueryResult<EntityAnalyticsPrivileges> => {
queryKey: string
): UseQueryResult<EntityAnalyticsPrivileges, SecurityAppError> => {
const { fetchAssetCriticalityPrivileges } = useEntityAnalyticsRoutes();
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const [isAssetCriticalityEnabled] = useUiSetting$<boolean>(ENABLE_ASSET_CRITICALITY_SETTING);
const isEnabled = isAssetCriticalityEnabled && hasEntityAnalyticsCapability;

return useQuery({
queryKey: [ASSET_CRITICALITY_KEY, PRIVILEGES_KEY, entityName, isEnabled],
queryKey: [ASSET_CRITICALITY_KEY, PRIVILEGES_KEY, queryKey, isEnabled],
queryFn: isEnabled ? fetchAssetCriticalityPrivileges : () => nonAuthorizedResponse,
});
};
Expand Down
Loading