[8.16] [Security Solution] [Onboarding] t1_analyst role blocked from interacting with cards due to missing integration privileges (#202413) (#202643)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[Security Solution] [Onboarding] t1_analyst role blocked from
interacting with cards due to missing integration privileges
(#202413)](https://github.com/elastic/kibana/pull/202413)

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

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

<!--BACKPORT [{"author":{"name":"Agustina Nahir
Ruidiaz","email":"61565784+agusruidiazgd@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-12-02T14:23:56Z","message":"[Security
Solution] [Onboarding] t1_analyst role blocked from interacting with
cards due to missing integration privileges (#202413)\n\n##
Summary\r\n\r\nThis PR temporarily fixes the #201799 issue for
Serverless.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/604128cb-49b0-4a93-9a15-2a5a0c511883\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nCheck the PR satisfies following conditions.
\r\n\r\nReviewers should verify this PR satisfies this list as
well.\r\n\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"3daaaa5b8c3e3a47b1e29f75df301b42d89caa87","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Threat
Hunting:Explore","ci:cloud-deploy","backport:version","v8.18.0"],"number":202413,"url":"https://github.com/elastic/kibana/pull/202413","mergeCommit":{"message":"[Security
Solution] [Onboarding] t1_analyst role blocked from interacting with
cards due to missing integration privileges (#202413)\n\n##
Summary\r\n\r\nThis PR temporarily fixes the #201799 issue for
Serverless.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/604128cb-49b0-4a93-9a15-2a5a0c511883\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nCheck the PR satisfies following conditions.
\r\n\r\nReviewers should verify this PR satisfies this list as
well.\r\n\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"3daaaa5b8c3e3a47b1e29f75df301b42d89caa87"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202413","number":202413,"mergeCommit":{"message":"[Security
Solution] [Onboarding] t1_analyst role blocked from interacting with
cards due to missing integration privileges (#202413)\n\n##
Summary\r\n\r\nThis PR temporarily fixes the #201799 issue for
Serverless.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/604128cb-49b0-4a93-9a15-2a5a0c511883\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nCheck the PR satisfies following conditions.
\r\n\r\nReviewers should verify this PR satisfies this list as
well.\r\n\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"3daaaa5b8c3e3a47b1e29f75df301b42d89caa87"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Agustina Nahir Ruidiaz 2024-12-03 13:49:41 +01:00 committed by GitHub
parent 38388ffed9
commit 048f45474c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 145 additions and 17 deletions

View file

@ -14,6 +14,7 @@ const props = {
checkComplete: jest.fn(),
isCardComplete: jest.fn(),
setExpandedCardId: jest.fn(),
isCardAvailable: jest.fn(),
};
describe('AlertsCard', () => {
@ -31,7 +32,8 @@ describe('AlertsCard', () => {
expect(getByTestId('alertsCardDescription')).toBeInTheDocument();
});
it('card callout should be rendered if integrations cards is not complete', () => {
it('card callout should be rendered if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByText } = render(
@ -43,7 +45,20 @@ describe('AlertsCard', () => {
expect(getByText('To view alerts add integrations first.')).toBeInTheDocument();
});
it('card button should be disabled if integrations cards is not complete', () => {
it('card callout should not be rendered if integrations card is not available', () => {
props.isCardAvailable.mockReturnValueOnce(false);
const { queryByText } = render(
<TestProviders>
<AlertsCard {...props} />
</TestProviders>
);
expect(queryByText('To view alerts add integrations first.')).not.toBeInTheDocument();
});
it('card button should be disabled if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByTestId } = render(
@ -54,4 +69,17 @@ describe('AlertsCard', () => {
expect(getByTestId('alertsCardButton').querySelector('button')).toBeDisabled();
});
it('card button should be enabled if integrations card is complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(true);
const { getByTestId } = render(
<TestProviders>
<AlertsCard {...props} />
</TestProviders>
);
expect(getByTestId('alertsCardButton').querySelector('button')).not.toBeDisabled();
});
});

View file

@ -20,12 +20,18 @@ export const AlertsCard: OnboardingCardComponent = ({
isCardComplete,
setExpandedCardId,
setComplete,
isCardAvailable,
}) => {
const isIntegrationsCardComplete = useMemo(
() => isCardComplete(OnboardingCardId.integrations),
[isCardComplete]
);
const isIntegrationsCardAvailable = useMemo(
() => isCardAvailable(OnboardingCardId.integrations),
[isCardAvailable]
);
const expandIntegrationsCard = useCallback(() => {
setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
}, [setExpandedCardId]);
@ -42,7 +48,7 @@ export const AlertsCard: OnboardingCardComponent = ({
<EuiText data-test-subj="alertsCardDescription" size="s" color="subdued">
{i18n.ALERTS_CARD_DESCRIPTION}
</EuiText>
{!isIntegrationsCardComplete && (
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
<>
<EuiSpacer size="m" />
<CardCallOut
@ -68,7 +74,7 @@ export const AlertsCard: OnboardingCardComponent = ({
onClick={() => setComplete(true)}
deepLinkId={SecurityPageName.alerts}
fill
isDisabled={!isIntegrationsCardComplete}
isDisabled={isIntegrationsCardAvailable && !isIntegrationsCardComplete}
>
{i18n.ALERTS_CARD_VIEW_ALERTS_BUTTON}
</SecuritySolutionLinkButton>

View file

@ -21,12 +21,18 @@ export const AssistantCard: OnboardingCardComponent<AssistantCardMetadata> = ({
setExpandedCardId,
checkCompleteMetadata,
checkComplete,
isCardAvailable,
}) => {
const isIntegrationsCardComplete = useMemo(
() => isCardComplete(OnboardingCardId.integrations),
[isCardComplete]
);
const isIntegrationsCardAvailable = useMemo(
() => isCardAvailable(OnboardingCardId.integrations),
[isCardAvailable]
);
const expandIntegrationsCard = useCallback(() => {
setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
}, [setExpandedCardId]);
@ -42,9 +48,7 @@ export const AssistantCard: OnboardingCardComponent<AssistantCardMetadata> = ({
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
{isIntegrationsCardComplete ? (
<ConnectorCards connectors={connectors} onConnectorSaved={checkComplete} />
) : (
{isIntegrationsCardAvailable && !isIntegrationsCardComplete ? (
<EuiFlexItem
className={css`
width: 45%;
@ -66,6 +70,8 @@ export const AssistantCard: OnboardingCardComponent<AssistantCardMetadata> = ({
}
/>
</EuiFlexItem>
) : (
<ConnectorCards connectors={connectors} onConnectorSaved={checkComplete} />
)}
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -14,6 +14,7 @@ const props = {
checkComplete: jest.fn(),
isCardComplete: jest.fn(),
setExpandedCardId: jest.fn(),
isCardAvailable: jest.fn(),
};
describe('RulesCard', () => {

View file

@ -17,9 +17,10 @@ const props = {
checkComplete: jest.fn(),
isCardComplete: jest.fn(),
setExpandedCardId: jest.fn(),
isCardAvailable: jest.fn(),
};
describe('RulesCard', () => {
describe('DashboardsCard', () => {
beforeEach(() => {
jest.clearAllMocks();
});
@ -34,7 +35,8 @@ describe('RulesCard', () => {
expect(getByTestId('dashboardsDescription')).toBeInTheDocument();
});
it('card callout should be rendered if integrations cards is not complete', () => {
it('card callout should be rendered if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByText } = render(
@ -46,7 +48,20 @@ describe('RulesCard', () => {
expect(getByText('To view dashboards add integrations first.')).toBeInTheDocument();
});
it('card button should be disabled if integrations cards is not complete', () => {
it('card callout should not be rendered if integrations card is not available', () => {
props.isCardAvailable.mockReturnValueOnce(false);
const { queryByText } = render(
<TestProviders>
<DashboardsCard {...props} />
</TestProviders>
);
expect(queryByText('To view dashboards add integrations first.')).not.toBeInTheDocument();
});
it('card button should be disabled if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByTestId } = render(
@ -57,7 +72,22 @@ describe('RulesCard', () => {
expect(getByTestId('dashboardsCardButton').querySelector('button')).toBeDisabled();
});
it('card button should be enabled if integrations card is complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(true);
const { getByTestId } = render(
<TestProviders>
<DashboardsCard {...props} />
</TestProviders>
);
expect(getByTestId('dashboardsCardButton').querySelector('button')).not.toBeDisabled();
});
it('should expand integrations card when callout link is clicked', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false); // To show the callout
const { getByTestId } = render(

View file

@ -20,12 +20,18 @@ export const DashboardsCard: OnboardingCardComponent = ({
isCardComplete,
setComplete,
setExpandedCardId,
isCardAvailable,
}) => {
const isIntegrationsCardComplete = useMemo(
() => isCardComplete(OnboardingCardId.integrations),
[isCardComplete]
);
const isIntegrationsCardAvailable = useMemo(
() => isCardAvailable(OnboardingCardId.integrations),
[isCardAvailable]
);
const expandIntegrationsCard = useCallback(() => {
setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
}, [setExpandedCardId]);
@ -45,7 +51,7 @@ export const DashboardsCard: OnboardingCardComponent = ({
<EuiText data-test-subj="dashboardsDescription" size="s" color="subdued">
{i18n.DASHBOARDS_CARD_DESCRIPTION}
</EuiText>
{!isIntegrationsCardComplete && (
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
<>
<EuiSpacer size="m" />
<CardCallOut
@ -76,7 +82,7 @@ export const DashboardsCard: OnboardingCardComponent = ({
cardId={OnboardingCardId.dashboards}
deepLinkId={SecurityPageName.dashboards}
fill
isDisabled={!isIntegrationsCardComplete}
isDisabled={isIntegrationsCardAvailable && !isIntegrationsCardComplete}
>
{i18n.DASHBOARDS_CARD_GO_TO_DASHBOARDS_BUTTON}
</CardLinkButton>

View file

@ -15,6 +15,7 @@ const props = {
checkComplete: jest.fn(),
isCardComplete: jest.fn(),
setExpandedCardId: jest.fn(),
isCardAvailable: jest.fn(),
};
describe('IntegrationsCard', () => {

View file

@ -13,6 +13,7 @@ const props = {
setComplete: jest.fn(),
checkComplete: jest.fn(),
isCardComplete: jest.fn(),
isCardAvailable: jest.fn(),
setExpandedCardId: jest.fn(),
};
@ -31,7 +32,8 @@ describe('RulesCard', () => {
expect(getByTestId('rulesCardDescription')).toBeInTheDocument();
});
it('card callout should be rendered if integrations cards is not complete', () => {
it('card callout should be rendered if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByText } = render(
@ -43,7 +45,20 @@ describe('RulesCard', () => {
expect(getByText('To add Elastic rules add integrations first.')).toBeInTheDocument();
});
it('card button should be disabled if integrations cards is not complete', () => {
it('card callout should not be rendered if integrations card is not available', () => {
props.isCardAvailable.mockReturnValueOnce(false);
const { queryByText } = render(
<TestProviders>
<RulesCard {...props} />
</TestProviders>
);
expect(queryByText('To add Elastic rules add integrations first.')).not.toBeInTheDocument();
});
it('card button should be disabled if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByTestId } = render(
@ -54,4 +69,17 @@ describe('RulesCard', () => {
expect(getByTestId('rulesCardButton').querySelector('button')).toBeDisabled();
});
it('card button should be enabled if integrations card is complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(true);
const { getByTestId } = render(
<TestProviders>
<RulesCard {...props} />
</TestProviders>
);
expect(getByTestId('rulesCardButton').querySelector('button')).not.toBeDisabled();
});
});

View file

@ -16,12 +16,21 @@ import { CardCallOut } from '../common/card_callout';
import rulesImageSrc from './images/rules.png';
import * as i18n from './translations';
export const RulesCard: OnboardingCardComponent = ({ isCardComplete, setExpandedCardId }) => {
export const RulesCard: OnboardingCardComponent = ({
isCardComplete,
setExpandedCardId,
isCardAvailable,
}) => {
const isIntegrationsCardComplete = useMemo(
() => isCardComplete(OnboardingCardId.integrations),
[isCardComplete]
);
const isIntegrationsCardAvailable = useMemo(
() => isCardAvailable(OnboardingCardId.integrations),
[isCardAvailable]
);
const expandIntegrationsCard = useCallback(() => {
setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
}, [setExpandedCardId]);
@ -38,7 +47,7 @@ export const RulesCard: OnboardingCardComponent = ({ isCardComplete, setExpanded
<EuiText data-test-subj="rulesCardDescription" size="s" color="subdued">
{i18n.RULES_CARD_DESCRIPTION}
</EuiText>
{!isIntegrationsCardComplete && (
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
<>
<EuiSpacer size="m" />
<CardCallOut
@ -63,7 +72,7 @@ export const RulesCard: OnboardingCardComponent = ({ isCardComplete, setExpanded
<SecuritySolutionLinkButton
deepLinkId={SecurityPageName.rules}
fill
isDisabled={!isIntegrationsCardComplete}
isDisabled={isIntegrationsCardAvailable && !isIntegrationsCardComplete}
>
{i18n.RULES_CARD_ADD_RULES_BUTTON}
</SecuritySolutionLinkButton>

View file

@ -14,6 +14,7 @@ import { OnboardingCardGroup } from './onboarding_card_group';
import { OnboardingCardPanel } from './onboarding_card_panel';
import { useExpandedCard } from './hooks/use_expanded_card';
import { useCompletedCards } from './hooks/use_completed_cards';
import type { IsCardAvailable } from '../../types';
export const OnboardingBody = React.memo(() => {
const bodyConfig = useBodyConfig();
@ -48,6 +49,12 @@ export const OnboardingBody = React.memo(() => {
[checkCardComplete]
);
const isCardAvailable = useCallback<IsCardAvailable>(
(cardId: OnboardingCardId) =>
bodyConfig.some((group) => group.cards.some((card) => card.id === cardId)),
[bodyConfig]
);
return (
<EuiFlexGroup direction="column" gutterSize="xl">
{bodyConfig.map((group, index) => (
@ -73,6 +80,7 @@ export const OnboardingBody = React.memo(() => {
setComplete={createSetCardComplete(id)}
checkComplete={createCheckCardComplete(id)}
isCardComplete={isCardComplete}
isCardAvailable={isCardAvailable}
setExpandedCardId={setExpandedCardId}
checkCompleteMetadata={cardCheckCompleteResult?.metadata}
/>

View file

@ -41,6 +41,7 @@ export type CheckCompleteResponse<TMetadata extends {} = {}> =
export type SetComplete = (isComplete: boolean) => void;
export type IsCardComplete = (cardId: OnboardingCardId) => boolean;
export type IsCardAvailable = (cardId: OnboardingCardId) => boolean;
export type SetExpandedCardId = (
cardId: OnboardingCardId | null,
options?: { scroll?: boolean }
@ -59,6 +60,10 @@ export type OnboardingCardComponent<TMetadata extends {} = {}> = React.Component
* Function to check if a specific card is complete.
*/
isCardComplete: IsCardComplete;
/**
* Function to check if a specific card is rendered.
*/
isCardAvailable: IsCardAvailable;
/**
* Function to expand a specific card ID and scroll to it.
*/