mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[EDR Workflows][Fleet] Search and display policy name for uninstall tokens (#176626)
## Summary In Fleet / Uninstall Tokens tab: - user can see the name of the related Agent Policy in a new column, if the policy still exists, <img width="1325" alt="image" src="1b06e39b
-103f-4439-9560-51227d75fb25"> - otherwise an info tooltip indicating why the name is missing. test: `This token's related Agent policy has already been deleted, so the policy name is unavailable.` <img width="1328" alt="image" src="d98a72d7
-9563-4d4e-9390-9d9826702026"> - Policy name (and the tooltip if missing) is indicated in Uninstall Command flyout, as well. <img width="300" alt="image" src="38d33f3e
-5f51-4c88-9efc-db9aed10ad42"><img width="300" alt="image" src="d1da0177
-5e21-44cb-9ef7-e768d02a00aa"> - User can search for Uninstall Tokens by policy ID and policy name as well: the search is a **case sensitive partial match**, excluding any special character (note: the text on the top of each screenshot has been updated, see first screenshot)     - a hint is added to indicate that a deleted agent policy's name is unknown test: `If an Agent policy is deleted, its policy name is also deleted. Use the policy ID to search for uninstall tokens related to deleted Agent policies.` <img width="1308" alt="image" src="816dae7e
-e3d3-445f-8ee1-48f93bd4f69a"> ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
78f61714ce
commit
a2d61ed0b1
19 changed files with 731 additions and 116 deletions
|
@ -8,6 +8,7 @@
|
|||
export interface UninstallToken {
|
||||
id: string;
|
||||
policy_id: string;
|
||||
policy_name: string | null;
|
||||
token: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { ListResult } from './common';
|
|||
export interface GetUninstallTokensMetadataRequest {
|
||||
query: {
|
||||
policyId?: string;
|
||||
search?: string;
|
||||
perPage?: number;
|
||||
page?: number;
|
||||
};
|
||||
|
|
|
@ -16,18 +16,25 @@ import { request } from '../tasks/common';
|
|||
import { login } from '../tasks/login';
|
||||
|
||||
describe('Uninstall token page', () => {
|
||||
before(() => {
|
||||
cleanupAgentPolicies();
|
||||
generatePolicies();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cleanupAgentPolicies();
|
||||
});
|
||||
[true, false].forEach((removePolicies) => {
|
||||
describe(`When ${
|
||||
removePolicies ? 'removing policies' : 'not removing policies'
|
||||
} before checking uninstall tokens`, () => {
|
||||
before(() => {
|
||||
cleanupAgentPolicies();
|
||||
generatePolicies();
|
||||
|
||||
if (removePolicies) {
|
||||
cleanupAgentPolicies();
|
||||
// Force page refresh after remove policies
|
||||
cy.visit('app/fleet/uninstall-tokens');
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cleanupAgentPolicies();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
login();
|
||||
|
||||
|
@ -38,12 +45,6 @@ describe('Uninstall token page', () => {
|
|||
.first()
|
||||
.then(($policyIdField) => $policyIdField[0].textContent)
|
||||
.as('policyIdInFirstLine');
|
||||
|
||||
if (removePolicies) {
|
||||
cleanupAgentPolicies();
|
||||
// Force page refresh after remove policies
|
||||
cy.visit('app/fleet/uninstall-tokens');
|
||||
}
|
||||
});
|
||||
|
||||
it('should show token by clicking on the eye button', () => {
|
||||
|
@ -75,7 +76,12 @@ describe('Uninstall token page', () => {
|
|||
cy.getBySel(UNINSTALL_TOKENS.UNINSTALL_COMMAND_FLYOUT).should('exist');
|
||||
|
||||
cy.contains(`sudo elastic-agent uninstall --uninstall-token ${fetchedToken.token}`);
|
||||
cy.contains(`Valid for the following agent policy: ${fetchedToken.policy_id}`);
|
||||
|
||||
cy.contains(
|
||||
`Valid for the following agent policy:${fetchedToken.policy_name || '-'} (${
|
||||
fetchedToken.policy_id
|
||||
})`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -86,6 +92,24 @@ describe('Uninstall token page', () => {
|
|||
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_TABLE_FIELD).should('have.length', 1);
|
||||
});
|
||||
|
||||
if (!removePolicies) {
|
||||
it('should filter for policy name by partial match', () => {
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_TABLE_FIELD).should('have.length.at.least', 3);
|
||||
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_SEARCH_FIELD).type('Agent 200');
|
||||
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_TABLE_FIELD).should('have.length', 1);
|
||||
});
|
||||
} else {
|
||||
it('should not be able to filter for policy name by partial match', () => {
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_TABLE_FIELD).should('have.length.at.least', 3);
|
||||
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_SEARCH_FIELD).type('Agent 200');
|
||||
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_TABLE_FIELD).should('have.length', 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -89,12 +89,14 @@ describe('UninstallTokenList page', () => {
|
|||
const uninstallTokenMetadataFixture1: UninstallTokenMetadata = {
|
||||
id: 'id-1',
|
||||
policy_id: 'policy-id-1',
|
||||
policy_name: 'Dummy Policy Name',
|
||||
created_at: '2023-06-19T08:47:31.457Z',
|
||||
};
|
||||
|
||||
const uninstallTokenMetadataFixture2: UninstallTokenMetadata = {
|
||||
id: 'id-2',
|
||||
policy_id: 'policy-id-2',
|
||||
policy_name: null,
|
||||
created_at: '2023-06-20T08:47:31.457Z',
|
||||
};
|
||||
|
||||
|
@ -103,16 +105,16 @@ describe('UninstallTokenList page', () => {
|
|||
token: '123456789',
|
||||
};
|
||||
|
||||
const getTokensResponseFixture: MockResponseType<GetUninstallTokensMetadataResponse> = {
|
||||
const generateGetUninstallTokensFixture = (items: UninstallTokenMetadata[]) => ({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
data: {
|
||||
items: [uninstallTokenMetadataFixture1, uninstallTokenMetadataFixture2],
|
||||
total: 2,
|
||||
items,
|
||||
total: items.length,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const getTokenResponseFixture: MockResponseType<GetUninstallTokenResponse> = {
|
||||
error: null,
|
||||
|
@ -121,7 +123,12 @@ describe('UninstallTokenList page', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useGetUninstallTokensMock.mockReturnValue(getTokensResponseFixture);
|
||||
useGetUninstallTokensMock.mockReturnValue(
|
||||
generateGetUninstallTokensFixture([
|
||||
uninstallTokenMetadataFixture1,
|
||||
uninstallTokenMetadataFixture2,
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should render table with token', () => {
|
||||
|
@ -131,6 +138,26 @@ describe('UninstallTokenList page', () => {
|
|||
expect(renderResult.queryByText('policy-id-1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should NOT show hint if Policy Name is found', () => {
|
||||
useGetUninstallTokensMock.mockReturnValue(
|
||||
generateGetUninstallTokensFixture([uninstallTokenMetadataFixture1])
|
||||
);
|
||||
const renderResult = render();
|
||||
|
||||
expect(renderResult.queryByTestId('emptyPolicyNameHint')).not.toBeInTheDocument();
|
||||
expect(renderResult.queryByText('Dummy Policy Name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show hint if Policy Name is not found', () => {
|
||||
useGetUninstallTokensMock.mockReturnValue(
|
||||
generateGetUninstallTokensFixture([uninstallTokenMetadataFixture2])
|
||||
);
|
||||
const renderResult = render();
|
||||
|
||||
expect(renderResult.queryByTestId('emptyPolicyNameHint')).toBeInTheDocument();
|
||||
expect(renderResult.queryByText('Dummy Policy Name')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide token by default', () => {
|
||||
const renderResult = render();
|
||||
|
||||
|
@ -168,7 +195,7 @@ describe('UninstallTokenList page', () => {
|
|||
expect(useGetUninstallTokenMock).toHaveBeenCalledWith(uninstallTokenFixture.id);
|
||||
});
|
||||
|
||||
it('should filter by policyID', async () => {
|
||||
it('should filter by policyID or policy name', async () => {
|
||||
const renderResult = render();
|
||||
|
||||
fireEvent.change(renderResult.getByTestId('uninstallTokensPolicyIdSearchInput'), {
|
||||
|
@ -178,7 +205,7 @@ describe('UninstallTokenList page', () => {
|
|||
expect(useGetUninstallTokensMock).toHaveBeenCalledWith({
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
policyId: 'searched policy id',
|
||||
search: 'searched policy id',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import type { CriteriaWithPagination, EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { EuiFieldSearch } from '@elastic/eui';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
|
@ -15,6 +17,8 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||
import { FormattedDate, FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { SendRequestResponse } from '@kbn/es-ui-shared-plugin/public';
|
||||
|
||||
import { EmptyPolicyNameHint } from '../../../../../components/uninstall_command_flyout/empty_policy_name_hint';
|
||||
|
||||
import { ApiKeyField } from '../../../../../components/api_key_field';
|
||||
import type { UninstallTokenMetadata } from '../../../../../../common/types/models/uninstall_token';
|
||||
import {
|
||||
|
@ -31,18 +35,15 @@ import {
|
|||
CREATED_AT_TITLE,
|
||||
VIEW_UNINSTALL_COMMAND_LABEL,
|
||||
POLICY_ID_TITLE,
|
||||
SEARCH_BY_POLICY_ID_PLACEHOLDER,
|
||||
SEARCH_BY_POLICY_ID_OR_NAME_PLACEHOLDER,
|
||||
TOKEN_TITLE,
|
||||
POLICY_NAME_TITLE,
|
||||
SEARCH_BY_POLICY_ID_OR_NAME_HINT,
|
||||
} from './translations';
|
||||
|
||||
const PolicyIdField = ({ policyId }: { policyId: string }) => (
|
||||
<EuiText
|
||||
size="s"
|
||||
className="eui-textTruncate"
|
||||
title={policyId}
|
||||
data-test-subj="uninstallTokensPolicyIdField"
|
||||
>
|
||||
{policyId}
|
||||
const TextField = ({ text, dataTestSubj }: { text: string; dataTestSubj?: string }) => (
|
||||
<EuiText size="s" className="eui-textTruncate" title={text} data-test-subj={dataTestSubj}>
|
||||
{text}
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
|
@ -74,7 +75,7 @@ const NoItemsMessage = ({ isLoading }: { isLoading: boolean }) =>
|
|||
export const UninstallTokenListPage = () => {
|
||||
useBreadcrumbs('uninstall_tokens');
|
||||
|
||||
const [policyIdSearch, setPolicyIdSearch] = useState<string>('');
|
||||
const [policyIdOrNameSearch, setPolicyIdOrNameSearch] = useState<string>('');
|
||||
const [tokenIdForFlyout, setTokenIdForFlyout] = useState<string | null>(null);
|
||||
|
||||
const { pagination, setPagination, pageSizeOptions } = usePagination();
|
||||
|
@ -82,7 +83,7 @@ export const UninstallTokenListPage = () => {
|
|||
const { isLoading, data } = useGetUninstallTokens({
|
||||
perPage: pagination.pageSize,
|
||||
page: pagination.currentPage,
|
||||
policyId: policyIdSearch,
|
||||
search: policyIdOrNameSearch,
|
||||
});
|
||||
|
||||
const tokens = data?.items ?? [];
|
||||
|
@ -90,10 +91,23 @@ export const UninstallTokenListPage = () => {
|
|||
|
||||
const columns: Array<EuiBasicTableColumn<UninstallTokenMetadata>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'policy_name',
|
||||
name: POLICY_NAME_TITLE,
|
||||
render: (policyName: string | null) => {
|
||||
if (policyName === null) {
|
||||
return <EmptyPolicyNameHint />;
|
||||
} else {
|
||||
return <TextField text={policyName} />;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'policy_id',
|
||||
name: POLICY_ID_TITLE,
|
||||
render: (policyId: string) => <PolicyIdField policyId={policyId} />,
|
||||
render: (policyId: string) => (
|
||||
<TextField text={policyId} dataTestSubj="uninstallTokensPolicyIdField" />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'created_at',
|
||||
|
@ -145,7 +159,7 @@ export const UninstallTokenListPage = () => {
|
|||
|
||||
const handleSearch = useCallback(
|
||||
(searchString: string): void => {
|
||||
setPolicyIdSearch(searchString);
|
||||
setPolicyIdOrNameSearch(searchString);
|
||||
setPagination((prevPagination) => ({ ...prevPagination, currentPage: 1 }));
|
||||
},
|
||||
[setPagination]
|
||||
|
@ -164,19 +178,26 @@ export const UninstallTokenListPage = () => {
|
|||
<EuiText color="subdued" size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.uninstallTokenList.pageDescription"
|
||||
defaultMessage="Uninstall token allows you to get the uninstall command if you need to uninstall the Agent/Endpoint on the Host."
|
||||
defaultMessage="An uninstall token allows you to use the uninstall command to remove Elastic Agent from a host."
|
||||
/>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFieldSearch
|
||||
onSearch={handleSearch}
|
||||
incremental
|
||||
fullWidth
|
||||
placeholder={SEARCH_BY_POLICY_ID_PLACEHOLDER}
|
||||
data-test-subj="uninstallTokensPolicyIdSearchInput"
|
||||
/>
|
||||
<EuiFlexGroup direction="row" alignItems="center">
|
||||
<EuiFieldSearch
|
||||
onSearch={handleSearch}
|
||||
incremental
|
||||
fullWidth
|
||||
maxLength={50}
|
||||
placeholder={SEARCH_BY_POLICY_ID_OR_NAME_PLACEHOLDER}
|
||||
data-test-subj="uninstallTokensPolicyIdSearchInput"
|
||||
/>
|
||||
|
||||
<EuiToolTip content={SEARCH_BY_POLICY_ID_OR_NAME_HINT}>
|
||||
<EuiIcon type="iInCircle" />
|
||||
</EuiToolTip>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
|
|
|
@ -11,6 +11,10 @@ export const POLICY_ID_TITLE = i18n.translate('xpack.fleet.uninstallTokenList.po
|
|||
defaultMessage: 'Policy ID',
|
||||
});
|
||||
|
||||
export const POLICY_NAME_TITLE = i18n.translate('xpack.fleet.uninstallTokenList.policyNameTitle', {
|
||||
defaultMessage: 'Policy name',
|
||||
});
|
||||
|
||||
export const CREATED_AT_TITLE = i18n.translate('xpack.fleet.uninstallTokenList.createdAtTitle', {
|
||||
defaultMessage: 'Created at',
|
||||
});
|
||||
|
@ -27,7 +31,15 @@ export const VIEW_UNINSTALL_COMMAND_LABEL = i18n.translate(
|
|||
{ defaultMessage: 'View uninstall command' }
|
||||
);
|
||||
|
||||
export const SEARCH_BY_POLICY_ID_PLACEHOLDER = i18n.translate(
|
||||
'xpack.fleet.uninstallTokenList.searchByPolicyPlaceholder',
|
||||
{ defaultMessage: 'Search by policy ID' }
|
||||
export const SEARCH_BY_POLICY_ID_OR_NAME_PLACEHOLDER = i18n.translate(
|
||||
'xpack.fleet.uninstallTokenList.searchByPolicyIdOrNamePlaceholder',
|
||||
{ defaultMessage: 'Search by policy ID or policy name' }
|
||||
);
|
||||
|
||||
export const SEARCH_BY_POLICY_ID_OR_NAME_HINT = i18n.translate(
|
||||
'xpack.fleet.uninstallTokenList.searchByPolicyIdOrNameHint',
|
||||
{
|
||||
defaultMessage:
|
||||
'If an Agent policy is deleted, its policy name is also deleted. Use the policy ID to search for uninstall tokens related to deleted Agent policies.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export const EMPTY_POLICY_NAME_HINT = i18n.translate(
|
||||
'xpack.fleet.uninstallTokenList.emptyPolicyNameHint',
|
||||
{
|
||||
defaultMessage:
|
||||
"This token's related Agent policy has already been deleted, so the policy name is unavailable.",
|
||||
}
|
||||
);
|
||||
|
||||
export const EmptyPolicyNameHint = () => (
|
||||
<>
|
||||
{'- '}
|
||||
<EuiToolTip content={EMPTY_POLICY_NAME_HINT}>
|
||||
<EuiIcon
|
||||
type="questionInCircle"
|
||||
color="subdued"
|
||||
aria-label={EMPTY_POLICY_NAME_HINT}
|
||||
data-test-subj="emptyPolicyNameHint"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</>
|
||||
);
|
|
@ -46,6 +46,7 @@ describe('UninstallCommandFlyout', () => {
|
|||
const uninstallTokenMetadataFixture: UninstallTokenMetadata = {
|
||||
id: 'id-1',
|
||||
policy_id: 'policy_id',
|
||||
policy_name: 'policy_name',
|
||||
created_at: '2023-06-19T08:47:31.457Z',
|
||||
};
|
||||
|
||||
|
@ -168,11 +169,32 @@ describe('UninstallCommandFlyout', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('displays the selected policy id to the user', () => {
|
||||
it('displays the selected policy id and policy name to the user', () => {
|
||||
const renderResult = render();
|
||||
|
||||
const policyIdHint = renderResult.getByTestId('uninstall-command-flyout-policy-id-hint');
|
||||
expect(policyIdHint.textContent).toBe('Valid for the following agent policy: policy_id');
|
||||
expect(policyIdHint.textContent).toBe(
|
||||
'Valid for the following agent policy:policy_name (policy_id)'
|
||||
);
|
||||
});
|
||||
|
||||
it('displays hint if policy name is missing', () => {
|
||||
const getTokenResponseFixture: MockResponseType<GetUninstallTokenResponse> = {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
data: {
|
||||
item: { ...uninstallTokenFixture, policy_name: null },
|
||||
},
|
||||
};
|
||||
useGetUninstallTokenMock.mockReturnValue(getTokenResponseFixture);
|
||||
|
||||
const renderResult = render();
|
||||
|
||||
const policyIdHint = renderResult.getByTestId('uninstall-command-flyout-policy-id-hint');
|
||||
expect(policyIdHint.textContent).toBe(
|
||||
"Valid for the following agent policy:- This token's related Agent policy has already been deleted, so the policy name is unavailable. (policy_id)"
|
||||
);
|
||||
expect(renderResult.getByTestId('emptyPolicyNameHint')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,10 +14,12 @@ import {
|
|||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import type { RequestError } from '../../hooks';
|
||||
import { useStartServices } from '../../hooks';
|
||||
|
@ -32,6 +34,7 @@ import {
|
|||
|
||||
import { UninstallCommandsPerPlatform } from './uninstall_commands_per_platform';
|
||||
import type { UninstallCommandTarget } from './types';
|
||||
import { EmptyPolicyNameHint } from './empty_policy_name_hint';
|
||||
|
||||
const UninstallAgentDescription = () => {
|
||||
const { docLinks } = useStartServices();
|
||||
|
@ -104,9 +107,11 @@ const ErrorFetchingUninstallToken = ({ error }: { error: RequestError | null })
|
|||
);
|
||||
|
||||
const UninstallCommandsByTokenId = ({ uninstallTokenId }: { uninstallTokenId: string }) => {
|
||||
const theme = useEuiTheme();
|
||||
const { isLoading, error, data } = useGetUninstallToken(uninstallTokenId);
|
||||
const token = data?.item.token;
|
||||
const policyId = data?.item.policy_id;
|
||||
const policyName = data?.item.policy_name;
|
||||
|
||||
return isLoading ? (
|
||||
<Loading size="l" />
|
||||
|
@ -118,12 +123,23 @@ const UninstallCommandsByTokenId = ({ uninstallTokenId }: { uninstallTokenId: st
|
|||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiText data-test-subj="uninstall-command-flyout-policy-id-hint">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUninstallCommandFlyout.validForPolicyId"
|
||||
defaultMessage="Valid for the following agent policy:"
|
||||
/>{' '}
|
||||
<EuiCode>{policyId}</EuiCode>
|
||||
<EuiText
|
||||
data-test-subj="uninstall-command-flyout-policy-id-hint"
|
||||
css={css`
|
||||
p {
|
||||
margin-block-end: ${theme.euiTheme.size.s};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUninstallCommandFlyout.validForPolicyId"
|
||||
defaultMessage="Valid for the following agent policy:"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
{policyName ?? <EmptyPolicyNameHint />} (<EuiCode>{policyId}</EuiCode>)
|
||||
</p>
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -61,9 +61,24 @@ describe('uninstall token handlers', () => {
|
|||
|
||||
describe('getUninstallTokensMetadataHandler', () => {
|
||||
const uninstallTokensFixture: UninstallTokenMetadata[] = [
|
||||
{ id: 'id-1', policy_id: 'policy-id-1', created_at: '2023-06-15T16:46:48.274Z' },
|
||||
{ id: 'id-2', policy_id: 'policy-id-2', created_at: '2023-06-15T16:46:48.274Z' },
|
||||
{ id: 'id-3', policy_id: 'policy-id-3', created_at: '2023-06-15T16:46:48.274Z' },
|
||||
{
|
||||
id: 'id-1',
|
||||
policy_id: 'policy-id-1',
|
||||
policy_name: null,
|
||||
created_at: '2023-06-15T16:46:48.274Z',
|
||||
},
|
||||
{
|
||||
id: 'id-2',
|
||||
policy_id: 'policy-id-2',
|
||||
policy_name: null,
|
||||
created_at: '2023-06-15T16:46:48.274Z',
|
||||
},
|
||||
{
|
||||
id: 'id-3',
|
||||
policy_id: 'policy-id-3',
|
||||
policy_name: null,
|
||||
created_at: '2023-06-15T16:46:48.274Z',
|
||||
},
|
||||
];
|
||||
|
||||
const uninstallTokensResponseFixture: GetUninstallTokensMetadataResponse = {
|
||||
|
@ -135,6 +150,7 @@ describe('uninstall token handlers', () => {
|
|||
const uninstallTokenFixture: UninstallToken = {
|
||||
id: 'id-1',
|
||||
policy_id: 'policy-id-1',
|
||||
policy_name: null,
|
||||
created_at: '2023-06-15T16:46:48.274Z',
|
||||
token: '123456789',
|
||||
};
|
||||
|
|
|
@ -32,6 +32,16 @@ export const getUninstallTokensMetadataHandler: FleetRequestHandler<
|
|||
return response.customError(UNINSTALL_TOKEN_SERVICE_UNAVAILABLE_ERROR);
|
||||
}
|
||||
|
||||
const { page = 1, perPage = 20, policyId, search } = request.query;
|
||||
|
||||
if (policyId && search) {
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: 'Query parameters `policyId` and `search` cannot be used at the same time.',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const fleetContext = await context.fleet;
|
||||
const soClient = fleetContext.internalSoClient;
|
||||
|
@ -44,10 +54,18 @@ export const getUninstallTokensMetadataHandler: FleetRequestHandler<
|
|||
|
||||
const managedPolicyIds = managedPolicies.map((policy) => policy.id);
|
||||
|
||||
const { page = 1, perPage = 20, policyId } = request.query;
|
||||
let policyIdSearchTerm: string | undefined;
|
||||
let policyNameSearchTerm: string | undefined;
|
||||
if (search) {
|
||||
policyIdSearchTerm = search.trim();
|
||||
policyNameSearchTerm = search.trim();
|
||||
} else if (policyId) {
|
||||
policyIdSearchTerm = policyId.trim();
|
||||
}
|
||||
|
||||
const body = await uninstallTokenService.getTokenMetadata(
|
||||
policyId?.trim(),
|
||||
policyIdSearchTerm,
|
||||
policyNameSearchTerm,
|
||||
page,
|
||||
perPage,
|
||||
managedPolicyIds.length > 0 ? managedPolicyIds : undefined
|
||||
|
|
|
@ -17,6 +17,7 @@ import { errors } from '@elastic/elasticsearch';
|
|||
|
||||
import { UninstallTokenError } from '../../../../common/errors';
|
||||
|
||||
import type { AgentPolicy } from '../../../../common';
|
||||
import { SO_SEARCH_LIMIT } from '../../../../common';
|
||||
|
||||
import type {
|
||||
|
@ -50,6 +51,7 @@ describe('UninstallTokenService', () => {
|
|||
let mockContext: MockedFleetAppContext;
|
||||
let mockBuckets: any[] = [];
|
||||
let uninstallTokenService: UninstallTokenServiceInterface;
|
||||
let getAgentPoliciesByIDsMock: jest.Mock;
|
||||
|
||||
function getDefaultSO(encrypted: boolean = true): TokenSO {
|
||||
return encrypted
|
||||
|
@ -194,6 +196,9 @@ describe('UninstallTokenService', () => {
|
|||
.getScopedClient({} as unknown as KibanaRequest) as jest.Mocked<SavedObjectsClientContract>;
|
||||
agentPolicyService.deployPolicies = jest.fn();
|
||||
|
||||
getAgentPoliciesByIDsMock = jest.fn().mockResolvedValue([]);
|
||||
agentPolicyService.getByIDs = getAgentPoliciesByIDsMock;
|
||||
|
||||
uninstallTokenService = new UninstallTokenService(esoClientMock);
|
||||
mockFind(canEncrypt);
|
||||
mockCreatePointInTimeFinder(canEncrypt);
|
||||
|
@ -231,12 +236,16 @@ describe('UninstallTokenService', () => {
|
|||
it('can correctly get one token', async () => {
|
||||
const so = getDefaultSO(canEncrypt);
|
||||
mockCreatePointInTimeFinderAsInternalUser([so]);
|
||||
getAgentPoliciesByIDsMock.mockResolvedValue([
|
||||
{ id: so.attributes.policy_id, name: 'cheese' },
|
||||
] as Array<Partial<AgentPolicy>>);
|
||||
|
||||
const token = await uninstallTokenService.getToken(so.id);
|
||||
|
||||
const expectedItem: UninstallToken = {
|
||||
id: so.id,
|
||||
policy_id: so.attributes.policy_id,
|
||||
policy_name: 'cheese',
|
||||
token: getToken(so, canEncrypt),
|
||||
created_at: so.created_at,
|
||||
};
|
||||
|
@ -250,6 +259,28 @@ describe('UninstallTokenService', () => {
|
|||
perPage: SO_SEARCH_LIMIT,
|
||||
}
|
||||
);
|
||||
expect(getAgentPoliciesByIDsMock).toHaveBeenCalledWith(
|
||||
soClientMock,
|
||||
[so.attributes.policy_id],
|
||||
{ ignoreMissing: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('sets `policy_name` to `null` if linked policy does not exist', async () => {
|
||||
const so = getDefaultSO(canEncrypt);
|
||||
mockCreatePointInTimeFinderAsInternalUser([so]);
|
||||
|
||||
const token = await uninstallTokenService.getToken(so.id);
|
||||
|
||||
const expectedItem: UninstallToken = {
|
||||
id: so.id,
|
||||
policy_id: so.attributes.policy_id,
|
||||
policy_name: null,
|
||||
token: getToken(so, canEncrypt),
|
||||
created_at: so.created_at,
|
||||
};
|
||||
|
||||
expect(token).toEqual(expectedItem);
|
||||
});
|
||||
|
||||
it('throws error if token is missing', async () => {
|
||||
|
@ -277,17 +308,22 @@ describe('UninstallTokenService', () => {
|
|||
it('can correctly get token metadata', async () => {
|
||||
const so = getDefaultSO(canEncrypt);
|
||||
const so2 = getDefaultSO2(canEncrypt);
|
||||
getAgentPoliciesByIDsMock.mockResolvedValue([
|
||||
{ id: so2.attributes.policy_id, name: 'only I have a name' },
|
||||
] as Array<Partial<AgentPolicy>>);
|
||||
|
||||
const actualItems = (await uninstallTokenService.getTokenMetadata()).items;
|
||||
const expectedItems: UninstallTokenMetadata[] = [
|
||||
{
|
||||
id: so.id,
|
||||
policy_id: so.attributes.policy_id,
|
||||
policy_name: null,
|
||||
created_at: so.created_at,
|
||||
},
|
||||
{
|
||||
id: so2.id,
|
||||
policy_id: so2.attributes.policy_id,
|
||||
policy_name: 'only I have a name',
|
||||
created_at: so2.created_at,
|
||||
},
|
||||
];
|
||||
|
@ -316,6 +352,44 @@ describe('UninstallTokenService', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareSearchString', () => {
|
||||
let prepareSearchString: (str: string | undefined, wildcard: string) => string;
|
||||
|
||||
beforeEach(() => {
|
||||
({ prepareSearchString } = uninstallTokenService as unknown as {
|
||||
prepareSearchString: typeof prepareSearchString;
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate search string with given wildcard', () => {
|
||||
expect(prepareSearchString('input', '*')).toEqual('*input*');
|
||||
expect(prepareSearchString('another', '.*')).toEqual('.*another.*');
|
||||
});
|
||||
|
||||
it('should remove special characters', () => {
|
||||
expect(prepareSearchString('_in:put', '*')).toEqual('*in*put*');
|
||||
expect(prepareSearchString('<input>', '*')).toEqual('*input*');
|
||||
expect(prepareSearchString('inp"ut"', '*')).toEqual('*inp*ut*');
|
||||
expect(prepareSearchString('"input"', '*')).toEqual('*input*');
|
||||
});
|
||||
|
||||
it('should replace multiple special characters with only one wildcard', () => {
|
||||
expect(prepareSearchString('<<<<inp"""""ut>>>>>', '*')).toEqual('*inp*ut*');
|
||||
});
|
||||
|
||||
it('should keep digits, letters and dash', () => {
|
||||
expect(prepareSearchString('123-ABC-XYZ-4567890', '*')).toEqual('*123-ABC-XYZ-4567890*');
|
||||
});
|
||||
|
||||
it('should return undefined if there are no useful characters', () => {
|
||||
expect(prepareSearchString('<<<<""""">>>>>', '*')).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined if input is undefined', () => {
|
||||
expect(prepareSearchString(undefined, '*')).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get hashed uninstall tokens', () => {
|
||||
|
|
|
@ -32,6 +32,8 @@ import type {
|
|||
|
||||
import { isResponseError } from '@kbn/es-errors';
|
||||
|
||||
import type { AgentPolicySOAttributes } from '../../../types';
|
||||
|
||||
import { UninstallTokenError } from '../../../../common/errors';
|
||||
|
||||
import type { GetUninstallTokensMetadataResponse } from '../../../../common/types/rest_spec/uninstall_token';
|
||||
|
@ -41,7 +43,11 @@ import type {
|
|||
UninstallTokenMetadata,
|
||||
} from '../../../../common/types/models/uninstall_token';
|
||||
|
||||
import { UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../../../constants';
|
||||
import {
|
||||
UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
SO_SEARCH_LIMIT,
|
||||
AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
} from '../../../constants';
|
||||
import { appContextService } from '../../app_context';
|
||||
import { agentPolicyService } from '../../agent_policy';
|
||||
|
||||
|
@ -74,19 +80,23 @@ export interface UninstallTokenServiceInterface {
|
|||
getToken(id: string): Promise<UninstallToken | null>;
|
||||
|
||||
/**
|
||||
* Get uninstall token metadata, optionally filtering by partial policyID, paginated
|
||||
* Get uninstall token metadata, optionally filtering for policyID and policy name, with a logical OR relation:
|
||||
* every uninstall token is returned with a related agent policy which partially matches either the given policyID or the policy name.
|
||||
* The result is paginated.
|
||||
*
|
||||
* @param policyIdFilter a string for partial matching the policyId
|
||||
* @param policyIdSearchTerm a string for partial matching the policyId
|
||||
* @param policyNameSearchTerm a string for partial matching the policy name
|
||||
* @param page
|
||||
* @param perPage
|
||||
* @param excludePolicyIds
|
||||
* @param excludedPolicyIds
|
||||
* @returns Uninstall Tokens Metadata Response
|
||||
*/
|
||||
getTokenMetadata(
|
||||
policyIdFilter?: string,
|
||||
policyIdSearchTerm?: string,
|
||||
policyNameSearchTerm?: string,
|
||||
page?: number,
|
||||
perPage?: number,
|
||||
excludePolicyIds?: string[]
|
||||
excludedPolicyIds?: string[]
|
||||
): Promise<GetUninstallTokensMetadataResponse>;
|
||||
|
||||
/**
|
||||
|
@ -171,45 +181,113 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
|
||||
const tokenObjects = await this.getDecryptedTokenObjects({ filter });
|
||||
|
||||
return tokenObjects.length === 1 ? this.convertTokenObjectToToken(tokenObjects[0]) : null;
|
||||
return tokenObjects.length === 1
|
||||
? this.convertTokenObjectToToken(
|
||||
await this.getPolicyIdNameDictionary([tokenObjects[0].attributes.policy_id]),
|
||||
tokenObjects[0]
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
private prepareSearchString(str: string | undefined, wildcard: string): string | undefined {
|
||||
const strWithoutSpecialCharacters = str
|
||||
?.split(/[^-\da-z]+/gi)
|
||||
.filter((x) => x)
|
||||
.join(wildcard);
|
||||
|
||||
return strWithoutSpecialCharacters
|
||||
? wildcard + strWithoutSpecialCharacters + wildcard
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private async searchPoliciesByName(policyNameSearchString: string): Promise<string[]> {
|
||||
const policyNameFilter = `${AGENT_POLICY_SAVED_OBJECT_TYPE}.attributes.name:${policyNameSearchString}`;
|
||||
|
||||
const agentPoliciesSOs = await this.soClient.find<AgentPolicySOAttributes>({
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
filter: policyNameFilter,
|
||||
});
|
||||
|
||||
return agentPoliciesSOs.saved_objects.map((attr) => attr.id);
|
||||
}
|
||||
|
||||
public async getTokenMetadata(
|
||||
policyIdFilter?: string,
|
||||
policyIdSearchTerm?: string,
|
||||
policyNameSearchTerm?: string,
|
||||
page = 1,
|
||||
perPage = 20,
|
||||
excludePolicyIds?: string[]
|
||||
excludedPolicyIds?: string[]
|
||||
): Promise<GetUninstallTokensMetadataResponse> {
|
||||
const includeFilter = policyIdFilter ? `.*${policyIdFilter}.*` : undefined;
|
||||
const policyIdFilter = this.prepareSearchString(policyIdSearchTerm, '.*');
|
||||
|
||||
const tokenObjects = await this.getTokenObjectsByIncludeFilter(includeFilter, excludePolicyIds);
|
||||
let policyIdsFoundByName: string[] | undefined;
|
||||
const policyNameSearchString = this.prepareSearchString(policyNameSearchTerm, '*');
|
||||
if (policyNameSearchString) {
|
||||
policyIdsFoundByName = await this.searchPoliciesByName(policyNameSearchString);
|
||||
}
|
||||
|
||||
const items: UninstallTokenMetadata[] = tokenObjects
|
||||
.slice((page - 1) * perPage, page * perPage)
|
||||
.map<UninstallTokenMetadata>(({ _id, _source }) => {
|
||||
let includeFilter: string | undefined;
|
||||
if (policyIdFilter || policyIdsFoundByName) {
|
||||
includeFilter = [
|
||||
...(policyIdsFoundByName ? policyIdsFoundByName : []),
|
||||
...(policyIdFilter ? [policyIdFilter] : []),
|
||||
].join('|');
|
||||
}
|
||||
|
||||
const tokenObjects = await this.getTokenObjectsByPolicyIdFilter(
|
||||
includeFilter,
|
||||
excludedPolicyIds
|
||||
);
|
||||
const tokenObjectsCurrentPage = tokenObjects.slice((page - 1) * perPage, page * perPage);
|
||||
const policyIds = tokenObjectsCurrentPage.map(
|
||||
(tokenObject) => tokenObject._source[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE].policy_id
|
||||
);
|
||||
const policyIdNameDictionary = await this.getPolicyIdNameDictionary(policyIds);
|
||||
|
||||
const items: UninstallTokenMetadata[] = tokenObjectsCurrentPage.map<UninstallTokenMetadata>(
|
||||
({ _id, _source }) => {
|
||||
this.assertPolicyId(_source[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE]);
|
||||
this.assertCreatedAt(_source.created_at);
|
||||
const policyId = _source[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE].policy_id;
|
||||
|
||||
return {
|
||||
id: _id.replace(`${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}:`, ''),
|
||||
policy_id: _source[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE].policy_id,
|
||||
policy_id: policyId,
|
||||
policy_name: policyIdNameDictionary[policyId] ?? null,
|
||||
created_at: _source.created_at,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return { items, total: tokenObjects.length, page, perPage };
|
||||
}
|
||||
|
||||
private async getPolicyIdNameDictionary(policyIds: string[]): Promise<Record<string, string>> {
|
||||
const agentPolicies = await agentPolicyService.getByIDs(this.soClient, policyIds, {
|
||||
ignoreMissing: true,
|
||||
});
|
||||
|
||||
return agentPolicies.reduce((dict, policy) => {
|
||||
dict[policy.id] = policy.name;
|
||||
return dict;
|
||||
}, {} as Record<string, string>);
|
||||
}
|
||||
|
||||
private async getDecryptedTokensForPolicyIds(policyIds: string[]): Promise<UninstallToken[]> {
|
||||
const tokenObjects = await this.getDecryptedTokenObjectsForPolicyIds(policyIds);
|
||||
const policyIdNameDictionary = await this.getPolicyIdNameDictionary(
|
||||
tokenObjects.map((obj) => obj.attributes.policy_id)
|
||||
);
|
||||
|
||||
return tokenObjects.map(this.convertTokenObjectToToken);
|
||||
return tokenObjects.map((tokenObject) =>
|
||||
this.convertTokenObjectToToken(policyIdNameDictionary, tokenObject)
|
||||
);
|
||||
}
|
||||
|
||||
private async getDecryptedTokenObjectsForPolicyIds(
|
||||
policyIds: string[]
|
||||
): Promise<Array<SavedObjectsFindResult<UninstallTokenSOAttributes>>> {
|
||||
const tokenObjectHits = await this.getTokenObjectsByIncludeFilter(policyIds);
|
||||
const tokenObjectHits = await this.getTokenObjectsByPolicyIdFilter(policyIds);
|
||||
|
||||
if (tokenObjectHits.length === 0) {
|
||||
return [];
|
||||
|
@ -280,12 +358,15 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
return tokenObjects;
|
||||
}
|
||||
|
||||
private convertTokenObjectToToken = ({
|
||||
id: _id,
|
||||
attributes,
|
||||
created_at: createdAt,
|
||||
error,
|
||||
}: SavedObjectsFindResult<UninstallTokenSOAttributes>): UninstallToken => {
|
||||
private convertTokenObjectToToken = (
|
||||
policyIdNameDictionary: Record<string, string>,
|
||||
{
|
||||
id: _id,
|
||||
attributes,
|
||||
created_at: createdAt,
|
||||
error,
|
||||
}: SavedObjectsFindResult<UninstallTokenSOAttributes>
|
||||
): UninstallToken => {
|
||||
if (error) {
|
||||
throw new UninstallTokenError(`Error when reading Uninstall Token with id '${_id}'.`);
|
||||
}
|
||||
|
@ -297,12 +378,13 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
return {
|
||||
id: _id,
|
||||
policy_id: attributes.policy_id,
|
||||
policy_name: policyIdNameDictionary[attributes.policy_id] ?? null,
|
||||
token: attributes.token || attributes.token_plain,
|
||||
created_at: createdAt,
|
||||
};
|
||||
};
|
||||
|
||||
private async getTokenObjectsByIncludeFilter(
|
||||
private async getTokenObjectsByPolicyIdFilter(
|
||||
include?: AggregationsTermsInclude,
|
||||
exclude?: AggregationsTermsExclude
|
||||
): Promise<Array<SearchHit<any>>> {
|
||||
|
@ -397,7 +479,7 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
const existingTokens = new Set();
|
||||
|
||||
if (!force) {
|
||||
(await this.getTokenObjectsByIncludeFilter(policyIds)).forEach((tokenObject) => {
|
||||
(await this.getTokenObjectsByPolicyIdFilter(policyIds)).forEach((tokenObject) => {
|
||||
existingTokens.add(tokenObject._source[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE].policy_id);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ import { schema } from '@kbn/config-schema';
|
|||
|
||||
export const GetUninstallTokensMetadataRequestSchema = {
|
||||
query: schema.object({
|
||||
policyId: schema.maybe(schema.string()),
|
||||
policyId: schema.maybe(schema.string({ maxLength: 50 })),
|
||||
search: schema.maybe(schema.string({ maxLength: 50 })),
|
||||
perPage: schema.maybe(schema.number({ defaultValue: 20, min: 5 })),
|
||||
page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })),
|
||||
}),
|
||||
|
|
|
@ -17710,7 +17710,6 @@
|
|||
"xpack.fleet.uninstallTokenList.loadingTokensMessage": "Chargement des jetons désinstallés...",
|
||||
"xpack.fleet.uninstallTokenList.pageDescription": "Les jetons désinstallés vous permettent d’obtenir la commande de désinstallation si vous devez désinstaller l’agent/le point de terminaison sur l’hôte.",
|
||||
"xpack.fleet.uninstallTokenList.policyIdTitle": "ID de stratégie",
|
||||
"xpack.fleet.uninstallTokenList.searchByPolicyPlaceholder": "Rechercher par ID de stratégie",
|
||||
"xpack.fleet.uninstallTokenList.tokenTitle": "Token",
|
||||
"xpack.fleet.uninstallTokenList.viewUninstallCommandLabel": "Voir la commande de désinstallation",
|
||||
"xpack.fleet.updateAgentTags.errorNotificationTitle": "Échec de la mise à jour des balises",
|
||||
|
|
|
@ -17723,7 +17723,6 @@
|
|||
"xpack.fleet.uninstallTokenList.loadingTokensMessage": "アンインストールトークンを読み込み中...",
|
||||
"xpack.fleet.uninstallTokenList.pageDescription": "アンインストールトークンは、ホスト上のエージェント/エンドポイントをアンインストールする必要がある場合に、アンインストールコマンドを取得します。",
|
||||
"xpack.fleet.uninstallTokenList.policyIdTitle": "ポリシーID",
|
||||
"xpack.fleet.uninstallTokenList.searchByPolicyPlaceholder": "ポリシーIDで検索",
|
||||
"xpack.fleet.uninstallTokenList.tokenTitle": "トークン",
|
||||
"xpack.fleet.uninstallTokenList.viewUninstallCommandLabel": "アンインストールコマンドを表示",
|
||||
"xpack.fleet.updateAgentTags.errorNotificationTitle": "タグの更新が失敗しました",
|
||||
|
|
|
@ -17817,7 +17817,6 @@
|
|||
"xpack.fleet.uninstallTokenList.loadingTokensMessage": "正在加载卸载令牌......",
|
||||
"xpack.fleet.uninstallTokenList.pageDescription": "如果需要卸载主机上的代理/终端,卸载令牌允许您获取卸载命令。",
|
||||
"xpack.fleet.uninstallTokenList.policyIdTitle": "策略 ID",
|
||||
"xpack.fleet.uninstallTokenList.searchByPolicyPlaceholder": "按策略 ID 搜索",
|
||||
"xpack.fleet.uninstallTokenList.tokenTitle": "令牌",
|
||||
"xpack.fleet.uninstallTokenList.viewUninstallCommandLabel": "查看卸载命令",
|
||||
"xpack.fleet.updateAgentTags.errorNotificationTitle": "标签更新失败",
|
||||
|
|
|
@ -10,7 +10,11 @@ import {
|
|||
GetUninstallTokensMetadataResponse,
|
||||
GetUninstallTokenResponse,
|
||||
} from '@kbn/fleet-plugin/common/types/rest_spec/uninstall_token';
|
||||
import { uninstallTokensRouteService } from '@kbn/fleet-plugin/common/services';
|
||||
import {
|
||||
agentPolicyRouteService,
|
||||
uninstallTokensRouteService,
|
||||
} from '@kbn/fleet-plugin/common/services';
|
||||
import { AgentPolicy } from '@kbn/fleet-plugin/common';
|
||||
import { testUsers } from '../test_users';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { addUninstallTokenToPolicy, generateNPolicies } from '../../helpers';
|
||||
|
@ -32,10 +36,13 @@ export default function (providerContext: FtrProviderContext) {
|
|||
|
||||
describe('GET uninstall_tokens', () => {
|
||||
describe('pagination', () => {
|
||||
let generatedPolicyIds: Set<string>;
|
||||
let generatedPolicies: Map<string, AgentPolicy>;
|
||||
|
||||
before(async () => {
|
||||
generatedPolicyIds = new Set(await generateNPolicies(supertest, 20));
|
||||
const generatedPoliciesArray = await generateNPolicies(supertest, 20);
|
||||
|
||||
generatedPolicies = new Map();
|
||||
generatedPoliciesArray.forEach((policy) => generatedPolicies.set(policy.id, policy));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -48,23 +55,27 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body.total).to.equal(generatedPolicies.size);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
|
||||
expect(body.items.length).to.equal(generatedPolicyIds.size);
|
||||
expect(body.items.length).to.equal(generatedPolicies.size);
|
||||
body.items.forEach(({ policy_id: policyId }) =>
|
||||
expect(generatedPolicyIds.has(policyId)).to.be(true)
|
||||
expect(generatedPolicies.has(policyId)).to.be(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return token metadata with creation date and id', async () => {
|
||||
it('should return token metadata with creation date, id, and correct policy name', async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.items[0]).to.have.property('policy_id');
|
||||
expect(body.items[0]).to.have.property('policy_name');
|
||||
expect(body.items[0].policy_name).to.equal(
|
||||
generatedPolicies.get(body.items[0].policy_id)?.name
|
||||
);
|
||||
expect(body.items[0]).to.have.property('created_at');
|
||||
expect(body.items[0]).to.have.property('id');
|
||||
|
||||
|
@ -75,13 +86,14 @@ export default function (providerContext: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should return default perPage number of token metadata if total is above default perPage', async () => {
|
||||
generatedPolicyIds.add((await generateNPolicies(supertest, 1))[0]);
|
||||
const additionalPolicy = (await generateNPolicies(supertest, 1))[0];
|
||||
generatedPolicies.set(additionalPolicy.id, additionalPolicy);
|
||||
|
||||
const response1 = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.expect(200);
|
||||
const body1: GetUninstallTokensMetadataResponse = response1.body;
|
||||
expect(body1.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body1.total).to.equal(generatedPolicies.size);
|
||||
expect(body1.page).to.equal(1);
|
||||
expect(body1.perPage).to.equal(20);
|
||||
expect(body1.items.length).to.equal(20);
|
||||
|
@ -91,7 +103,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.query({ page: 2 })
|
||||
.expect(200);
|
||||
const body2: GetUninstallTokensMetadataResponse = response2.body;
|
||||
expect(body2.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body2.total).to.equal(generatedPolicies.size);
|
||||
expect(body2.page).to.equal(2);
|
||||
expect(body2.perPage).to.equal(20);
|
||||
expect(body2.items.length).to.equal(1);
|
||||
|
@ -110,7 +122,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body.total).to.equal(generatedPolicies.size);
|
||||
expect(body.perPage).to.equal(8);
|
||||
expect(body.page).to.equal(i);
|
||||
|
||||
|
@ -118,9 +130,9 @@ export default function (providerContext: FtrProviderContext) {
|
|||
receivedPolicyIds.push(...receivedIds);
|
||||
}
|
||||
|
||||
expect(receivedPolicyIds.length).to.equal(generatedPolicyIds.size);
|
||||
expect(receivedPolicyIds.length).to.equal(generatedPolicies.size);
|
||||
receivedPolicyIds.forEach((policyId) =>
|
||||
expect(generatedPolicyIds.has(policyId)).to.be(true)
|
||||
expect(generatedPolicies.has(policyId)).to.be(true)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -151,15 +163,15 @@ export default function (providerContext: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('when there are multiple tokens for a policy', () => {
|
||||
let generatedPolicyIdsArray: string[];
|
||||
let generatedPolicies: AgentPolicy[];
|
||||
let timestampBeforeAddingNewTokens: number;
|
||||
|
||||
before(async () => {
|
||||
generatedPolicyIdsArray = await generateNPolicies(supertest, 20);
|
||||
generatedPolicies = await generateNPolicies(supertest, 20);
|
||||
|
||||
timestampBeforeAddingNewTokens = Date.now();
|
||||
|
||||
const savingAdditionalTokensPromises = generatedPolicyIdsArray.map((id) =>
|
||||
const savingAdditionalTokensPromises = generatedPolicies.map(({ id }) =>
|
||||
addUninstallTokenToPolicy(kibanaServer, id, `${id} latest token`)
|
||||
);
|
||||
|
||||
|
@ -176,7 +188,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(generatedPolicyIdsArray.length);
|
||||
expect(body.total).to.equal(generatedPolicies.length);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
|
||||
|
@ -187,11 +199,44 @@ export default function (providerContext: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when `policyId` query param is used', () => {
|
||||
let generatedPolicyIdsArray: string[];
|
||||
describe('when there are managed policies', () => {
|
||||
let notManagedPolicies: AgentPolicy[];
|
||||
let managedPolicies: AgentPolicy[];
|
||||
|
||||
before(async () => {
|
||||
generatedPolicyIdsArray = await generateNPolicies(supertest, 5);
|
||||
notManagedPolicies = await generateNPolicies(supertest, 3);
|
||||
managedPolicies = await generateNPolicies(supertest, 4, { is_managed: true });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('should not return token metadata for managed policies', async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(notManagedPolicies.length);
|
||||
|
||||
const returnedPolicyIds = new Set();
|
||||
body.items.forEach((uninstallToken) => returnedPolicyIds.add(uninstallToken.policy_id));
|
||||
|
||||
notManagedPolicies.forEach((notManagedPolicy) => {
|
||||
expect(returnedPolicyIds.has(notManagedPolicy.id)).to.be(true);
|
||||
});
|
||||
managedPolicies.forEach((managedPolicy) => {
|
||||
expect(returnedPolicyIds.has(managedPolicy.id)).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `policyId` query param is used', () => {
|
||||
let generatedPolicyArray: AgentPolicy[];
|
||||
|
||||
before(async () => {
|
||||
generatedPolicyArray = await generateNPolicies(supertest, 5);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -199,7 +244,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should return token metadata for full policyID if found', async () => {
|
||||
const selectedPolicyId = generatedPolicyIdsArray[3];
|
||||
const selectedPolicyId = generatedPolicyArray[3].id;
|
||||
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
|
@ -216,7 +261,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should return token metadata for partial policyID if found', async () => {
|
||||
const selectedPolicyId = generatedPolicyIdsArray[2];
|
||||
const selectedPolicyId = generatedPolicyArray[2].id;
|
||||
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
|
@ -232,6 +277,23 @@ export default function (providerContext: FtrProviderContext) {
|
|||
expect(body.items[0].policy_id).to.equal(selectedPolicyId);
|
||||
});
|
||||
|
||||
it('should not return token metadata by policy name', async () => {
|
||||
const selectedPolicyName = generatedPolicyArray[2].name;
|
||||
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
policyId: selectedPolicyName,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(0);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
expect(body.items).to.eql([]);
|
||||
});
|
||||
|
||||
it('should return nothing if policy is not found', async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
|
@ -248,6 +310,207 @@ export default function (providerContext: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when `search` query param is used', () => {
|
||||
let generatedManagedPolicyArray: AgentPolicy[];
|
||||
let generatedPolicyArray: AgentPolicy[];
|
||||
|
||||
before(async () => {
|
||||
generatedPolicyArray = await generateNPolicies(supertest, 8);
|
||||
generatedPolicyArray.push(
|
||||
...(await generateNPolicies(supertest, 1, { name: 'Special: Policy' }))
|
||||
);
|
||||
generatedPolicyArray.push(
|
||||
...(await generateNPolicies(supertest, 1, { name: 'Special<Policy' }))
|
||||
);
|
||||
generatedManagedPolicyArray = await generateNPolicies(supertest, 3, { is_managed: true });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('should return token metadata for full policyID', async () => {
|
||||
const selectedPolicyId = generatedPolicyArray[3].id;
|
||||
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
search: selectedPolicyId,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(1);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
expect(body.items[0].policy_id).to.equal(selectedPolicyId);
|
||||
});
|
||||
|
||||
it('should return token metadata for partial policyID', async () => {
|
||||
const selectedPolicyId = generatedPolicyArray[2].id;
|
||||
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
search: selectedPolicyId.slice(4, 11),
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(1);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
expect(body.items[0].policy_id).to.equal(selectedPolicyId);
|
||||
});
|
||||
|
||||
it('should return token metadata for policyID even if policy is deleted', async () => {
|
||||
const deletedPolicy = (await generateNPolicies(supertest, 1))[0];
|
||||
await supertest
|
||||
.post(agentPolicyRouteService.getDeletePath())
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ agentPolicyId: deletedPolicy.id })
|
||||
.expect(200);
|
||||
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
search: deletedPolicy.id,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(1);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
expect(body.items[0].policy_id).to.equal(deletedPolicy.id);
|
||||
expect(body.items[0].policy_name).to.equal(null);
|
||||
});
|
||||
|
||||
it('should return token metadata for full policy name', async () => {
|
||||
const selectedPolicy = generatedPolicyArray[6];
|
||||
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
search: selectedPolicy.name,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(1);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
expect(body.items[0].policy_id).to.equal(selectedPolicy.id);
|
||||
});
|
||||
|
||||
it('should return token metadata for partial policy name', async () => {
|
||||
const selectedPolicy = generatedPolicyArray[1];
|
||||
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
search: selectedPolicy.name.slice(4),
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(1);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
expect(body.items[0].policy_id).to.equal(selectedPolicy.id);
|
||||
});
|
||||
|
||||
it('should return nothing if policy is not found', async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
search: 'not-existing-policy-id-or-name',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(0);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
expect(body.items).to.eql([]);
|
||||
});
|
||||
|
||||
it('should return nothing if searched for managed policy id', async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
search: generatedManagedPolicyArray[0].id,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(0);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
expect(body.items).to.eql([]);
|
||||
});
|
||||
|
||||
it('should return nothing if searched for managed policy name', async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
search: generatedManagedPolicyArray[0].name,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(0);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
expect(body.items).to.eql([]);
|
||||
});
|
||||
|
||||
it('should return nothing if searched for managed policy name', async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
search: generatedManagedPolicyArray[0].name,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(0);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
expect(body.items).to.eql([]);
|
||||
});
|
||||
|
||||
it('should remove special characters', async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
search: 'Special Policy',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(2);
|
||||
const returnedPolicyNames = body.items.map((item) => item.policy_name);
|
||||
expect(returnedPolicyNames).to.contain('Special: Policy');
|
||||
expect(returnedPolicyNames).to.contain('Special<Policy');
|
||||
});
|
||||
|
||||
it('should return 400 if both `search` and `policyId` are used', async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
policyId: 'policy id',
|
||||
search: 'policy name',
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
const body = response.body;
|
||||
expect(body.message).to.equal(
|
||||
'Query parameters `policyId` and `search` cannot be used at the same time.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
it('should return 200 if the user has FLEET ALL (and INTEGRATIONS READ) privilege', async () => {
|
||||
const { username, password } = testUsers.fleet_all_int_read;
|
||||
|
@ -293,6 +556,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
|
||||
expect(body.item.id).to.equal(generatedUninstallTokenId);
|
||||
expect(body.item.policy_id).to.equal('the policy id');
|
||||
expect(body.item.policy_name).to.equal(null);
|
||||
expect(body.item.token).to.equal('the token');
|
||||
expect(body.item).to.have.property('created_at');
|
||||
});
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
import * as uuid from 'uuid';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { agentPolicyRouteService } from '@kbn/fleet-plugin/common/services';
|
||||
import { CreateAgentPolicyResponse } from '@kbn/fleet-plugin/common';
|
||||
import {
|
||||
AgentPolicy,
|
||||
CreateAgentPolicyRequest,
|
||||
CreateAgentPolicyResponse,
|
||||
} from '@kbn/fleet-plugin/common';
|
||||
import { KbnClient } from '@kbn/test';
|
||||
import { UNINSTALL_TOKENS_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
|
||||
import { FtrProviderContext } from '../api_integration/ftr_provider_context';
|
||||
|
@ -120,7 +124,11 @@ export function setPrereleaseSetting(supertest: any) {
|
|||
});
|
||||
}
|
||||
|
||||
export const generateNPolicies = async (supertest: any, number: number) => {
|
||||
export const generateNPolicies = async (
|
||||
supertest: any,
|
||||
number: number,
|
||||
overwrite?: Partial<CreateAgentPolicyRequest['body']>
|
||||
): Promise<AgentPolicy[]> => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < number; i++) {
|
||||
|
@ -128,15 +136,14 @@ export const generateNPolicies = async (supertest: any, number: number) => {
|
|||
supertest
|
||||
.post(agentPolicyRouteService.getCreatePath())
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ name: `Agent Policy ${uuid.v4()}`, namespace: 'default' })
|
||||
.send({ name: `Agent Policy ${uuid.v4()}`, namespace: 'default', ...overwrite })
|
||||
.expect(200)
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
const policyIds = responses.map(({ body }) => (body as CreateAgentPolicyResponse).item.id);
|
||||
|
||||
return policyIds;
|
||||
return responses.map(({ body }) => (body as CreateAgentPolicyResponse).item);
|
||||
};
|
||||
|
||||
export const addUninstallTokenToPolicy = async (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue