[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)


![image](a5461186-a063-4dc3-8f50-3a6fd0af626d)


![image](2ccaffd7-1358-4113-8e4b-a930d5e20d0f)


![image](d6840f08-ac4f-4d72-b827-abbff25c146a)


![image](c18f4437-1fc9-4c4e-b846-21b68651ebb9)


- 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:
Gergő Ábrahám 2024-02-13 12:25:33 +01:00 committed by GitHub
parent 78f61714ce
commit a2d61ed0b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 731 additions and 116 deletions

View file

@ -8,6 +8,7 @@
export interface UninstallToken {
id: string;
policy_id: string;
policy_name: string | null;
token: string;
created_at: string;
}

View file

@ -12,6 +12,7 @@ import type { ListResult } from './common';
export interface GetUninstallTokensMetadataRequest {
query: {
policyId?: string;
search?: string;
perPage?: number;
page?: number;
};

View file

@ -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);
});
}
});
});

View file

@ -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',
});
});
});

View file

@ -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" />

View file

@ -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.',
}
);

View file

@ -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>
</>
);

View file

@ -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();
});
});

View file

@ -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>
</>
);

View file

@ -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',
};

View file

@ -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

View file

@ -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', () => {

View file

@ -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);
});
}

View file

@ -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 })),
}),

View file

@ -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 dobtenir la commande de désinstallation si vous devez désinstaller lagent/le point de terminaison sur lhô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",

View file

@ -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": "タグの更新が失敗しました",

View file

@ -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": "标签更新失败",

View file

@ -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');
});

View file

@ -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 (