mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Fleet][Agent tamper protection ] Split GET /uninstall_tokens
API (#159944)
## Summary > **Note** > For testing: enable the `agentTamperProtectionEnabled` feature flag. This PR modifies the quite new `GET /api/fleet/uninstall_tokens` API: - `GET /api/fleet/uninstall_tokens`, returns token 'metadata' (i.e. **uninstall token id**, policy ID and creation date) for the latest token for every policy, without the token itself. - it is paginated (query params `page`, `perPage`), - and can be searched by partial policy ID (query param `policyId`). - this route is not used at the moment, will be used very soon - `GET /api/fleet/uninstall_tokens/{id}` returns one decrypted token identified by its ID - ~`GET /api/fleet/agent_policies/{policyId}/uninstall_tokens`, returns the decrypted token history for one policy~ - ~this route is used by the `UninstallCommandFlyout`~ - this was added and then removed, because not a necessity at the moment, and let's keep open all doors for agent tampering v2 ### Todo - done ✅ `created_at` field was removed from the uninstall token saved object mapping (21855ce37b320e1864c5b9db647ac2355158f91d), because it was unused and messed up ordering by the saved object's own `created_at` field. This removal is not allowed, though, so this issue needs to be fixed. **Update:** after a discussion with Kibana Core team, the `created_at` field was removed in a separate PR which is merged in v8.9.0. Reason: it's okay to use the SO's internal `created_at` field for sorting. Also, the mapping will be released in v8.9.0 first, so it's okay to modify it this time. The PR: https://github.com/elastic/kibana/pull/159985 ### Checklist Delete any items that are not applicable to this PR. - [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 --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a5620cdb98
commit
c9b6054cbe
20 changed files with 957 additions and 484 deletions
|
@ -110,6 +110,7 @@ const STANDARD_LIST_TYPES = [
|
|||
'epm-packages-assets',
|
||||
'fleet-preconfiguration-deletion-record',
|
||||
'fleet-fleet-server-host',
|
||||
'fleet-uninstall-tokens',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -101,7 +101,14 @@ export const getHttp = (basepath = BASE_PATH) => {
|
|||
};
|
||||
}
|
||||
|
||||
if (path.match('/api/fleet/uninstall_tokens')) {
|
||||
if (path.match('/api/fleet/uninstall_tokens/token-id-13')) {
|
||||
return {
|
||||
item: { token: '123-456-789' },
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
};
|
||||
} else if (path.match('/api/fleet/uninstall_tokens')) {
|
||||
if (options.query?.policyId === 'missing-policy') {
|
||||
return {
|
||||
items: [],
|
||||
|
@ -111,7 +118,7 @@ export const getHttp = (basepath = BASE_PATH) => {
|
|||
};
|
||||
} else {
|
||||
return {
|
||||
items: [{ token: '123-456-789' }],
|
||||
items: [{ id: 'token-id-13' }],
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
|
|
|
@ -171,6 +171,7 @@ export const ENROLLMENT_API_KEY_ROUTES = {
|
|||
|
||||
export const UNINSTALL_TOKEN_ROUTES = {
|
||||
LIST_PATTERN: `${API_ROOT}/uninstall_tokens`,
|
||||
INFO_PATTERN: `${API_ROOT}/uninstall_tokens/{uninstallTokenId}`,
|
||||
};
|
||||
|
||||
// Agents setup API routes
|
||||
|
|
|
@ -22,3 +22,4 @@ export class MessageSigningError extends FleetError {}
|
|||
|
||||
export class FleetActionsError extends FleetError {}
|
||||
export class FleetActionsClientError extends FleetError {}
|
||||
export class UninstallTokenError extends FleetError {}
|
||||
|
|
|
@ -286,6 +286,8 @@ export const enrollmentAPIKeyRouteService = {
|
|||
|
||||
export const uninstallTokensRouteService = {
|
||||
getListPath: () => UNINSTALL_TOKEN_ROUTES.LIST_PATTERN,
|
||||
getInfoPath: (uninstallTokenId: string) =>
|
||||
UNINSTALL_TOKEN_ROUTES.INFO_PATTERN.replace('{uninstallTokenId}', uninstallTokenId),
|
||||
};
|
||||
|
||||
export const setupRouteService = {
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
*/
|
||||
|
||||
export interface UninstallToken {
|
||||
id: string;
|
||||
policy_id: string;
|
||||
token: string;
|
||||
created_at?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export type UninstallTokenMetadata = Omit<UninstallToken, 'token'>;
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { UninstallToken } from '../models/uninstall_token';
|
||||
import type { UninstallToken, UninstallTokenMetadata } from '../models/uninstall_token';
|
||||
|
||||
import type { ListResult } from './common';
|
||||
|
||||
export interface GetUninstallTokensRequest {
|
||||
export interface GetUninstallTokensMetadataRequest {
|
||||
query: {
|
||||
policyId?: string;
|
||||
perPage?: number;
|
||||
|
@ -17,4 +17,14 @@ export interface GetUninstallTokensRequest {
|
|||
};
|
||||
}
|
||||
|
||||
export type GetUninstallTokensResponse = ListResult<UninstallToken>;
|
||||
export type GetUninstallTokensMetadataResponse = ListResult<UninstallTokenMetadata>;
|
||||
|
||||
export interface GetUninstallTokenRequest {
|
||||
params: {
|
||||
uninstallTokenId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetUninstallTokenResponse {
|
||||
item: UninstallToken;
|
||||
}
|
||||
|
|
|
@ -9,11 +9,22 @@ import React from 'react';
|
|||
|
||||
import type { UseRequestResponse } from '@kbn/es-ui-shared-plugin/public';
|
||||
|
||||
import type { GetUninstallTokensResponse } from '../../../common/types/rest_spec/uninstall_token';
|
||||
import type {
|
||||
UninstallToken,
|
||||
UninstallTokenMetadata,
|
||||
} from '../../../common/types/models/uninstall_token';
|
||||
|
||||
import type {
|
||||
GetUninstallTokensMetadataResponse,
|
||||
GetUninstallTokenResponse,
|
||||
} from '../../../common/types/rest_spec/uninstall_token';
|
||||
|
||||
import { createFleetTestRendererMock } from '../../mock';
|
||||
|
||||
import { useGetUninstallTokens } from '../../hooks/use_request/uninstall_tokens';
|
||||
import {
|
||||
useGetUninstallTokens,
|
||||
useGetUninstallToken,
|
||||
} from '../../hooks/use_request/uninstall_tokens';
|
||||
|
||||
import type { RequestError } from '../../hooks';
|
||||
|
||||
|
@ -21,13 +32,29 @@ import type { UninstallCommandFlyoutProps } from './uninstall_command_flyout';
|
|||
import { UninstallCommandFlyout } from './uninstall_command_flyout';
|
||||
|
||||
jest.mock('../../hooks/use_request/uninstall_tokens', () => ({
|
||||
useGetUninstallToken: jest.fn(),
|
||||
useGetUninstallTokens: jest.fn(),
|
||||
}));
|
||||
|
||||
type MockReturnType = Partial<UseRequestResponse<GetUninstallTokensResponse, RequestError>>;
|
||||
type MockResponseType<DataType> = Pick<
|
||||
UseRequestResponse<DataType, RequestError>,
|
||||
'data' | 'error' | 'isLoading'
|
||||
>;
|
||||
|
||||
describe('UninstallCommandFlyout', () => {
|
||||
const uninstallTokenMetadataFixture: UninstallTokenMetadata = {
|
||||
id: 'id-1',
|
||||
policy_id: 'policy_id',
|
||||
created_at: '2023-06-19T08:47:31.457Z',
|
||||
};
|
||||
|
||||
const uninstallTokenFixture: UninstallToken = {
|
||||
...uninstallTokenMetadataFixture,
|
||||
token: '123456789',
|
||||
};
|
||||
|
||||
const useGetUninstallTokensMock = useGetUninstallTokens as jest.Mock;
|
||||
const useGetUninstallTokenMock = useGetUninstallToken as jest.Mock;
|
||||
|
||||
const render = (props: Partial<UninstallCommandFlyoutProps> = {}) => {
|
||||
const renderer = createFleetTestRendererMock();
|
||||
|
@ -38,20 +65,26 @@ describe('UninstallCommandFlyout', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const response: GetUninstallTokensResponse = {
|
||||
items: [{ policy_id: 'policy_id', token: '123456789' }],
|
||||
total: 1,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
};
|
||||
|
||||
const mockReturn: MockReturnType = {
|
||||
const getTokensResponseFixture: MockResponseType<GetUninstallTokensMetadataResponse> = {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
data: response,
|
||||
data: {
|
||||
items: [uninstallTokenMetadataFixture],
|
||||
total: 1,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
},
|
||||
};
|
||||
useGetUninstallTokensMock.mockReturnValue(getTokensResponseFixture);
|
||||
|
||||
useGetUninstallTokensMock.mockReturnValue(mockReturn);
|
||||
const getTokenResponseFixture: MockResponseType<GetUninstallTokenResponse> = {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
data: {
|
||||
item: uninstallTokenFixture,
|
||||
},
|
||||
};
|
||||
useGetUninstallTokenMock.mockReturnValue(getTokenResponseFixture);
|
||||
});
|
||||
|
||||
describe('uninstall command targets', () => {
|
||||
|
@ -72,9 +105,9 @@ describe('UninstallCommandFlyout', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when fetching the tokens is successful', () => {
|
||||
describe('when fetching the token is successful', () => {
|
||||
it('shows loading spinner while fetching', () => {
|
||||
const mockReturn: MockReturnType = {
|
||||
const mockReturn: MockResponseType<GetUninstallTokensMetadataResponse> = {
|
||||
isLoading: true,
|
||||
error: null,
|
||||
data: null,
|
||||
|
@ -132,9 +165,9 @@ describe('UninstallCommandFlyout', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when fetching the tokens is unsuccessful', () => {
|
||||
describe('when fetching the token metadata is unsuccessful', () => {
|
||||
it('shows error message when fetching returns an error', () => {
|
||||
const mockReturn: MockReturnType = {
|
||||
const mockReturn: MockResponseType<GetUninstallTokensMetadataResponse> = {
|
||||
isLoading: false,
|
||||
error: new Error('received error message'),
|
||||
data: null,
|
||||
|
@ -150,7 +183,7 @@ describe('UninstallCommandFlyout', () => {
|
|||
});
|
||||
|
||||
it('shows "Unknown error" error message when token is missing from response', () => {
|
||||
const mockReturn: MockReturnType = {
|
||||
const mockReturn: MockResponseType<GetUninstallTokensMetadataResponse> = {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
data: null,
|
||||
|
@ -165,4 +198,38 @@ describe('UninstallCommandFlyout', () => {
|
|||
expect(renderResult.queryByText(/Unknown error/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when fetching the decrypted token is unsuccessful', () => {
|
||||
it('shows error message when fetching returns an error', () => {
|
||||
const mockReturn: MockResponseType<GetUninstallTokenResponse> = {
|
||||
isLoading: false,
|
||||
error: new Error('received error message'),
|
||||
data: null,
|
||||
};
|
||||
useGetUninstallTokenMock.mockReturnValue(mockReturn);
|
||||
|
||||
const renderResult = render();
|
||||
|
||||
expect(renderResult.queryByTestId('loadingSpinner')).not.toBeInTheDocument();
|
||||
|
||||
expect(renderResult.queryByText(/Unable to fetch uninstall token/)).toBeInTheDocument();
|
||||
expect(renderResult.queryByText(/received error message/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Unknown error" error message when token is missing from response', () => {
|
||||
const mockReturn: MockResponseType<GetUninstallTokenResponse> = {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
data: null,
|
||||
};
|
||||
useGetUninstallTokenMock.mockReturnValue(mockReturn);
|
||||
|
||||
const renderResult = render();
|
||||
|
||||
expect(renderResult.queryByTestId('loadingSpinner')).not.toBeInTheDocument();
|
||||
|
||||
expect(renderResult.queryByText(/Unable to fetch uninstall token/)).toBeInTheDocument();
|
||||
expect(renderResult.queryByText(/Unknown error/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,13 +19,17 @@ import React from 'react';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { RequestError } from '../../hooks';
|
||||
import { useStartServices } from '../../hooks';
|
||||
|
||||
import { useGetUninstallTokens } from '../../hooks/use_request/uninstall_tokens';
|
||||
|
||||
import { Error } from '../error';
|
||||
import { Loading } from '../loading';
|
||||
|
||||
import {
|
||||
useGetUninstallToken,
|
||||
useGetUninstallTokens,
|
||||
} from '../../hooks/use_request/uninstall_tokens';
|
||||
|
||||
import { UninstallCommandsPerPlatform } from './uninstall_commands_per_platform';
|
||||
import type { UninstallCommandTarget } from './types';
|
||||
|
||||
|
@ -82,35 +86,47 @@ const UninstallEndpointDescription = () => (
|
|||
</>
|
||||
);
|
||||
|
||||
const UninstallCommands = ({ policyId }: { policyId: string }) => {
|
||||
const { data, error, isLoading } = useGetUninstallTokens({ policyId });
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading size="l" />;
|
||||
}
|
||||
|
||||
const token: string | null = data?.items?.[0]?.token ?? null;
|
||||
|
||||
if (error || !token) {
|
||||
return (
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUninstallCommandFlyout.errorFetchingToken"
|
||||
defaultMessage="Unable to fetch uninstall token"
|
||||
/>
|
||||
}
|
||||
error={
|
||||
error ??
|
||||
i18n.translate('xpack.fleet.agentUninstallCommandFlyout.unknownError', {
|
||||
defaultMessage: 'Unknown error',
|
||||
})
|
||||
}
|
||||
const ErrorFetchingUninstallToken = ({ error }: { error: RequestError | null }) => (
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUninstallCommandFlyout.errorFetchingToken"
|
||||
defaultMessage="Unable to fetch uninstall token"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
error={
|
||||
error ??
|
||||
i18n.translate('xpack.fleet.agentUninstallCommandFlyout.unknownError', {
|
||||
defaultMessage: 'Unknown error',
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return <UninstallCommandsPerPlatform token={token} />;
|
||||
const UninstallCommandsByTokenId = ({ uninstallTokenId }: { uninstallTokenId: string }) => {
|
||||
const { isLoading, error, data } = useGetUninstallToken(uninstallTokenId);
|
||||
const token = data?.item.token;
|
||||
|
||||
return isLoading ? (
|
||||
<Loading size="l" />
|
||||
) : error || !token ? (
|
||||
<ErrorFetchingUninstallToken error={error} />
|
||||
) : (
|
||||
<UninstallCommandsPerPlatform token={token} />
|
||||
);
|
||||
};
|
||||
|
||||
const UninstallCommandsByPolicyId = ({ policyId }: { policyId: string }) => {
|
||||
const { isLoading, error, data } = useGetUninstallTokens({ policyId });
|
||||
const tokenId = data?.items?.[0]?.id;
|
||||
|
||||
return isLoading ? (
|
||||
<Loading size="l" />
|
||||
) : error || !tokenId ? (
|
||||
<ErrorFetchingUninstallToken error={error} />
|
||||
) : (
|
||||
<UninstallCommandsByTokenId uninstallTokenId={tokenId} />
|
||||
);
|
||||
};
|
||||
|
||||
export interface UninstallCommandFlyoutProps {
|
||||
|
@ -144,7 +160,7 @@ export const UninstallCommandFlyout: React.FunctionComponent<UninstallCommandFly
|
|||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<UninstallCommands policyId={policyId} />
|
||||
<UninstallCommandsByPolicyId policyId={policyId} />
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
import { uninstallTokensRouteService } from '../../../common/services';
|
||||
|
||||
import type {
|
||||
GetUninstallTokensRequest,
|
||||
GetUninstallTokensResponse,
|
||||
GetUninstallTokensMetadataRequest,
|
||||
GetUninstallTokensMetadataResponse,
|
||||
GetUninstallTokenResponse,
|
||||
} from '../../../common/types/rest_spec/uninstall_token';
|
||||
|
||||
import { useRequest } from './use_request';
|
||||
|
@ -18,16 +19,22 @@ export const useGetUninstallTokens = ({
|
|||
policyId,
|
||||
page,
|
||||
perPage,
|
||||
}: GetUninstallTokensRequest['query'] = {}) => {
|
||||
const query: GetUninstallTokensRequest['query'] = {
|
||||
}: GetUninstallTokensMetadataRequest['query'] = {}) => {
|
||||
const query: GetUninstallTokensMetadataRequest['query'] = {
|
||||
policyId,
|
||||
page,
|
||||
perPage,
|
||||
};
|
||||
|
||||
return useRequest<GetUninstallTokensResponse>({
|
||||
return useRequest<GetUninstallTokensMetadataResponse>({
|
||||
method: 'get',
|
||||
path: uninstallTokensRouteService.getListPath(),
|
||||
query,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUninstallToken = (uninstallTokenId: string) =>
|
||||
useRequest<GetUninstallTokenResponse>({
|
||||
method: 'get',
|
||||
path: uninstallTokensRouteService.getInfoPath(uninstallTokenId),
|
||||
});
|
||||
|
|
|
@ -15,6 +15,8 @@ import type {
|
|||
} from '@kbn/core/server';
|
||||
import type { KibanaRequest } from '@kbn/core/server';
|
||||
|
||||
import { UninstallTokenError } from '../../common/errors';
|
||||
|
||||
import { appContextService } from '../services';
|
||||
|
||||
import {
|
||||
|
@ -82,6 +84,9 @@ const getHTTPResponseCode = (error: FleetError): number => {
|
|||
if (error instanceof PackagePolicyNameExistsError) {
|
||||
return 409; // Conflict
|
||||
}
|
||||
if (error instanceof UninstallTokenError) {
|
||||
return 500; // Internal Error
|
||||
}
|
||||
return 400; // Bad Request
|
||||
};
|
||||
|
||||
|
|
|
@ -186,10 +186,8 @@ export function createMessageSigningServiceMock(): MessageSigningServiceInterfac
|
|||
|
||||
export function createUninstallTokenServiceMock(): UninstallTokenServiceInterface {
|
||||
return {
|
||||
getTokenForPolicyId: jest.fn(),
|
||||
getTokensForPolicyIds: jest.fn(),
|
||||
getAllTokens: jest.fn(),
|
||||
findTokensForPartialPolicyId: jest.fn(),
|
||||
getToken: jest.fn(),
|
||||
getTokenMetadata: jest.fn(),
|
||||
getHashedTokenForPolicyId: jest.fn(),
|
||||
getHashedTokensForPolicyIds: jest.fn(),
|
||||
getAllHashedTokens: jest.fn(),
|
||||
|
|
|
@ -12,46 +12,41 @@ import { httpServerMock, coreMock } from '@kbn/core/server/mocks';
|
|||
import type { RouterMock } from '@kbn/core-http-router-server-mocks';
|
||||
import { mockRouter } from '@kbn/core-http-router-server-mocks';
|
||||
|
||||
import type { GetUninstallTokensResponse } from '../../../common/types/rest_spec/uninstall_token';
|
||||
import type {
|
||||
UninstallToken,
|
||||
UninstallTokenMetadata,
|
||||
} from '../../../common/types/models/uninstall_token';
|
||||
|
||||
import type {
|
||||
GetUninstallTokenRequest,
|
||||
GetUninstallTokensMetadataResponse,
|
||||
} from '../../../common/types/rest_spec/uninstall_token';
|
||||
|
||||
import type { FleetRequestHandlerContext } from '../..';
|
||||
|
||||
import type { MockedFleetAppContext } from '../../mocks';
|
||||
import { createAppContextStartContractMock, xpackMocks } from '../../mocks';
|
||||
import { appContextService } from '../../services';
|
||||
import type { GetUninstallTokensRequestSchema } from '../../types/rest_spec/uninstall_token';
|
||||
import type {
|
||||
GetUninstallTokenRequestSchema,
|
||||
GetUninstallTokensMetadataRequestSchema,
|
||||
} from '../../types/rest_spec/uninstall_token';
|
||||
|
||||
import { registerRoutes } from '.';
|
||||
|
||||
import { getUninstallTokensHandler } from './handlers';
|
||||
import { getUninstallTokenHandler, getUninstallTokensMetadataHandler } from './handlers';
|
||||
|
||||
describe('getUninstallTokensHandler', () => {
|
||||
describe('uninstall token handlers', () => {
|
||||
let context: FleetRequestHandlerContext;
|
||||
let request: KibanaRequest<unknown, TypeOf<typeof GetUninstallTokensRequestSchema.query>>;
|
||||
let response: ReturnType<typeof httpServerMock.createResponseFactory>;
|
||||
let appContextStartContractMock: MockedFleetAppContext;
|
||||
let getAllTokensMock: jest.Mock;
|
||||
|
||||
const uninstallTokensResponseFixture: GetUninstallTokensResponse = {
|
||||
items: [
|
||||
{ policy_id: 'policy-id-1', token: '123456' },
|
||||
{ policy_id: 'policy-id-2', token: 'abcdef' },
|
||||
{ policy_id: 'policy-id-3', token: '9876543210' },
|
||||
],
|
||||
total: 3,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
context = coreMock.createCustomRequestHandlerContext(xpackMocks.createRequestHandlerContext());
|
||||
response = httpServerMock.createResponseFactory();
|
||||
request = httpServerMock.createKibanaRequest();
|
||||
|
||||
appContextStartContractMock = createAppContextStartContractMock();
|
||||
|
||||
appContextService.start(appContextStartContractMock);
|
||||
getAllTokensMock = appContextService.getUninstallTokenService()?.getAllTokens as jest.Mock;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -59,40 +54,132 @@ describe('getUninstallTokensHandler', () => {
|
|||
appContextService.stop();
|
||||
});
|
||||
|
||||
it('should return uninstall tokens for all policies', async () => {
|
||||
getAllTokensMock.mockResolvedValue(uninstallTokensResponseFixture);
|
||||
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' },
|
||||
];
|
||||
|
||||
await getUninstallTokensHandler(context, request, response);
|
||||
const uninstallTokensResponseFixture: GetUninstallTokensMetadataResponse = {
|
||||
items: uninstallTokensFixture,
|
||||
total: 3,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
};
|
||||
|
||||
expect(response.ok).toHaveBeenCalledWith({
|
||||
body: uninstallTokensResponseFixture,
|
||||
let getTokenMetadataMock: jest.Mock;
|
||||
let request: KibanaRequest<
|
||||
unknown,
|
||||
TypeOf<typeof GetUninstallTokensMetadataRequestSchema.query>
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
const uninstallTokenService = appContextService.getUninstallTokenService()!;
|
||||
getTokenMetadataMock = uninstallTokenService.getTokenMetadata as jest.Mock;
|
||||
|
||||
request = httpServerMock.createKibanaRequest();
|
||||
});
|
||||
|
||||
it('should return uninstall tokens for all policies', async () => {
|
||||
getTokenMetadataMock.mockResolvedValue(uninstallTokensResponseFixture);
|
||||
|
||||
await getUninstallTokensMetadataHandler(context, request, response);
|
||||
|
||||
expect(response.ok).toHaveBeenCalledWith({
|
||||
body: uninstallTokensResponseFixture,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return internal error when uninstallTokenService is unavailable', async () => {
|
||||
appContextService.stop();
|
||||
appContextService.start({
|
||||
...appContextStartContractMock,
|
||||
// @ts-expect-error
|
||||
uninstallTokenService: undefined,
|
||||
});
|
||||
|
||||
await getUninstallTokensMetadataHandler(context, request, response);
|
||||
|
||||
expect(response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 500,
|
||||
body: { message: 'Uninstall Token Service is unavailable.' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return internal error when uninstallTokenService throws error', async () => {
|
||||
getTokenMetadataMock.mockRejectedValue(Error('something happened'));
|
||||
|
||||
await getUninstallTokensMetadataHandler(context, request, response);
|
||||
|
||||
expect(response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 500,
|
||||
body: { message: 'something happened' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return internal error when uninstallTokenService is unavailable', async () => {
|
||||
appContextService.stop();
|
||||
appContextService.start({
|
||||
...appContextStartContractMock,
|
||||
// @ts-expect-error
|
||||
uninstallTokenService: undefined,
|
||||
describe('getUninstallTokenHandler', () => {
|
||||
const uninstallTokenFixture: UninstallToken = {
|
||||
id: 'id-1',
|
||||
policy_id: 'policy-id-1',
|
||||
created_at: '2023-06-15T16:46:48.274Z',
|
||||
token: '123456789',
|
||||
};
|
||||
|
||||
let getTokenMock: jest.Mock;
|
||||
let request: KibanaRequest<TypeOf<typeof GetUninstallTokenRequestSchema.params>>;
|
||||
|
||||
beforeEach(() => {
|
||||
const uninstallTokenService = appContextService.getUninstallTokenService()!;
|
||||
getTokenMock = uninstallTokenService.getToken as jest.Mock;
|
||||
|
||||
const requestOptions: GetUninstallTokenRequest = {
|
||||
params: {
|
||||
uninstallTokenId: uninstallTokenFixture.id,
|
||||
},
|
||||
};
|
||||
request = httpServerMock.createKibanaRequest(requestOptions);
|
||||
});
|
||||
|
||||
await getUninstallTokensHandler(context, request, response);
|
||||
it('should return requested uninstall token', async () => {
|
||||
getTokenMock.mockResolvedValue(uninstallTokenFixture);
|
||||
|
||||
expect(response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 500,
|
||||
body: { message: 'Uninstall Token Service is unavailable.' },
|
||||
await getUninstallTokenHandler(context, request, response);
|
||||
|
||||
expect(getTokenMock).toHaveBeenCalledWith(uninstallTokenFixture.id);
|
||||
expect(response.ok).toHaveBeenCalledWith({
|
||||
body: {
|
||||
item: uninstallTokenFixture,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return internal error when uninstallTokenService throws error', async () => {
|
||||
getAllTokensMock.mockRejectedValue(Error('something happened'));
|
||||
it('should return internal error when uninstallTokenService is unavailable', async () => {
|
||||
appContextService.stop();
|
||||
appContextService.start({
|
||||
...appContextStartContractMock,
|
||||
// @ts-expect-error
|
||||
uninstallTokenService: undefined,
|
||||
});
|
||||
|
||||
await getUninstallTokensHandler(context, request, response);
|
||||
await getUninstallTokenHandler(context, request, response);
|
||||
|
||||
expect(response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 500,
|
||||
body: { message: 'something happened' },
|
||||
expect(response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 500,
|
||||
body: { message: 'Uninstall Token Service is unavailable.' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return internal error when uninstallTokenService throws error', async () => {
|
||||
getTokenMock.mockRejectedValue(Error('something happened'));
|
||||
|
||||
await getUninstallTokenHandler(context, request, response);
|
||||
|
||||
expect(response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 500,
|
||||
body: { message: 'something happened' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -104,15 +191,19 @@ describe('getUninstallTokensHandler', () => {
|
|||
router = mockRouter.create();
|
||||
});
|
||||
|
||||
it('should register handler if feature flag is enabled', () => {
|
||||
it('should register handlers if feature flag is enabled', () => {
|
||||
config = { enableExperimental: ['agentTamperProtectionEnabled'] };
|
||||
|
||||
registerRoutes(router, config);
|
||||
|
||||
expect(router.get).toHaveBeenCalledWith(expect.any(Object), getUninstallTokensHandler);
|
||||
expect(router.get).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
getUninstallTokensMetadataHandler
|
||||
);
|
||||
expect(router.get).toHaveBeenCalledWith(expect.any(Object), getUninstallTokenHandler);
|
||||
});
|
||||
|
||||
it('should NOT register handler if feature flag is disabled', async () => {
|
||||
it('should NOT register handlers if feature flag is disabled', async () => {
|
||||
config = { enableExperimental: [] };
|
||||
|
||||
registerRoutes(router, config);
|
||||
|
|
|
@ -6,34 +6,64 @@
|
|||
*/
|
||||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import type { CustomHttpResponseOptions, ResponseError } from '@kbn/core-http-server';
|
||||
|
||||
import { appContextService } from '../../services';
|
||||
import type { FleetRequestHandler } from '../../types';
|
||||
import type { GetUninstallTokensResponse } from '../../../common/types/rest_spec/uninstall_token';
|
||||
import type { GetUninstallTokensRequestSchema } from '../../types/rest_spec/uninstall_token';
|
||||
import type {
|
||||
GetUninstallTokensMetadataRequestSchema,
|
||||
GetUninstallTokenRequestSchema,
|
||||
} from '../../types/rest_spec/uninstall_token';
|
||||
import { defaultFleetErrorHandler } from '../../errors';
|
||||
import type { GetUninstallTokenResponse } from '../../../common/types/rest_spec/uninstall_token';
|
||||
|
||||
export const getUninstallTokensHandler: FleetRequestHandler<
|
||||
const UNINSTALL_TOKEN_SERVICE_UNAVAILABLE_ERROR: CustomHttpResponseOptions<ResponseError> = {
|
||||
statusCode: 500,
|
||||
body: { message: 'Uninstall Token Service is unavailable.' },
|
||||
};
|
||||
|
||||
export const getUninstallTokensMetadataHandler: FleetRequestHandler<
|
||||
unknown,
|
||||
TypeOf<typeof GetUninstallTokensRequestSchema.query>
|
||||
TypeOf<typeof GetUninstallTokensMetadataRequestSchema.query>
|
||||
> = async (context, request, response) => {
|
||||
const uninstallTokenService = appContextService.getUninstallTokenService();
|
||||
if (!uninstallTokenService) {
|
||||
return response.customError({
|
||||
statusCode: 500,
|
||||
body: { message: 'Uninstall Token Service is unavailable.' },
|
||||
});
|
||||
return response.customError(UNINSTALL_TOKEN_SERVICE_UNAVAILABLE_ERROR);
|
||||
}
|
||||
|
||||
try {
|
||||
const { page = 1, perPage = 20, policyId } = request.query;
|
||||
|
||||
let body: GetUninstallTokensResponse;
|
||||
if (policyId) {
|
||||
body = await uninstallTokenService.findTokensForPartialPolicyId(policyId, page, perPage);
|
||||
} else {
|
||||
body = await uninstallTokenService.getAllTokens(page, perPage);
|
||||
}
|
||||
const body = await uninstallTokenService.getTokenMetadata(policyId?.trim(), page, perPage);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (error) {
|
||||
return defaultFleetErrorHandler({ error, response });
|
||||
}
|
||||
};
|
||||
|
||||
export const getUninstallTokenHandler: FleetRequestHandler<
|
||||
TypeOf<typeof GetUninstallTokenRequestSchema.params>
|
||||
> = async (context, request, response) => {
|
||||
const uninstallTokenService = appContextService.getUninstallTokenService();
|
||||
if (!uninstallTokenService) {
|
||||
return response.customError(UNINSTALL_TOKEN_SERVICE_UNAVAILABLE_ERROR);
|
||||
}
|
||||
|
||||
try {
|
||||
const { uninstallTokenId } = request.params;
|
||||
|
||||
const token = await uninstallTokenService.getToken(uninstallTokenId);
|
||||
|
||||
if (token === null) {
|
||||
return response.notFound({
|
||||
body: { message: `Uninstall Token not found with id ${uninstallTokenId}` },
|
||||
});
|
||||
}
|
||||
|
||||
const body: GetUninstallTokenResponse = {
|
||||
item: token,
|
||||
};
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (error) {
|
||||
|
|
|
@ -8,10 +8,13 @@ import { UNINSTALL_TOKEN_ROUTES } from '../../../common/constants';
|
|||
import type { FleetConfigType } from '../../config';
|
||||
|
||||
import type { FleetAuthzRouter } from '../../services/security';
|
||||
import { GetUninstallTokensRequestSchema } from '../../types/rest_spec/uninstall_token';
|
||||
import {
|
||||
GetUninstallTokenRequestSchema,
|
||||
GetUninstallTokensMetadataRequestSchema,
|
||||
} from '../../types/rest_spec/uninstall_token';
|
||||
import { parseExperimentalConfigValue } from '../../../common/experimental_features';
|
||||
|
||||
import { getUninstallTokensHandler } from './handlers';
|
||||
import { getUninstallTokenHandler, getUninstallTokensMetadataHandler } from './handlers';
|
||||
|
||||
export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType) => {
|
||||
const experimentalFeatures = parseExperimentalConfigValue(config.enableExperimental);
|
||||
|
@ -20,12 +23,23 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType
|
|||
router.get(
|
||||
{
|
||||
path: UNINSTALL_TOKEN_ROUTES.LIST_PATTERN,
|
||||
validate: GetUninstallTokensRequestSchema,
|
||||
validate: GetUninstallTokensMetadataRequestSchema,
|
||||
fleetAuthz: {
|
||||
fleet: { all: true },
|
||||
},
|
||||
},
|
||||
getUninstallTokensHandler
|
||||
getUninstallTokensMetadataHandler
|
||||
);
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: UNINSTALL_TOKEN_ROUTES.INFO_PATTERN,
|
||||
validate: GetUninstallTokenRequestSchema,
|
||||
fleetAuthz: {
|
||||
fleet: { all: true },
|
||||
},
|
||||
},
|
||||
getUninstallTokenHandler
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,11 +8,17 @@
|
|||
import { createHash } from 'crypto';
|
||||
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
|
||||
import type { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
|
||||
import type { UninstallToken } from '../../../../common/types/models/uninstall_token';
|
||||
import { SO_SEARCH_LIMIT } from '../../../../common';
|
||||
|
||||
import type {
|
||||
UninstallToken,
|
||||
UninstallTokenMetadata,
|
||||
} from '../../../../common/types/models/uninstall_token';
|
||||
|
||||
import { UNINSTALL_TOKENS_SAVED_OBJECT_TYPE } from '../../../constants';
|
||||
import { createAppContextStartContractMock, type MockedFleetAppContext } from '../../../mocks';
|
||||
|
@ -22,6 +28,7 @@ import { agentPolicyService } from '../../agent_policy';
|
|||
import { UninstallTokenService, type UninstallTokenServiceInterface } from '.';
|
||||
|
||||
describe('UninstallTokenService', () => {
|
||||
const now = new Date().toISOString();
|
||||
const aDayAgo = new Date(Date.now() - 24 * 3600 * 1000).toISOString();
|
||||
|
||||
let soClientMock: jest.Mocked<SavedObjectsClientContract>;
|
||||
|
@ -38,6 +45,7 @@ describe('UninstallTokenService', () => {
|
|||
policy_id: 'test-policy-id',
|
||||
token: 'test-token',
|
||||
},
|
||||
created_at: now,
|
||||
}
|
||||
: {
|
||||
id: 'test-so-id',
|
||||
|
@ -45,6 +53,7 @@ describe('UninstallTokenService', () => {
|
|||
policy_id: 'test-policy-id',
|
||||
token_plain: 'test-token-plain',
|
||||
},
|
||||
created_at: now,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -80,6 +89,10 @@ describe('UninstallTokenService', () => {
|
|||
{
|
||||
_id: defaultSO.id,
|
||||
...defaultSO,
|
||||
_source: {
|
||||
[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE]: defaultSO.attributes,
|
||||
created_at: defaultSO.created_at,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -94,6 +107,7 @@ describe('UninstallTokenService', () => {
|
|||
_id: defaultSO2.id,
|
||||
...defaultSO2,
|
||||
_source: {
|
||||
[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE]: defaultSO2.attributes,
|
||||
created_at: defaultSO2.created_at,
|
||||
},
|
||||
},
|
||||
|
@ -185,52 +199,74 @@ describe('UninstallTokenService', () => {
|
|||
});
|
||||
|
||||
describe('get uninstall tokens', () => {
|
||||
it('can correctly getTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO(canEncrypt);
|
||||
const token = await uninstallTokenService.getTokenForPolicyId(so.attributes.policy_id);
|
||||
expect(token).toEqual({
|
||||
policy_id: so.attributes.policy_id,
|
||||
token: getToken(so, canEncrypt),
|
||||
} as UninstallToken);
|
||||
});
|
||||
describe('getToken', () => {
|
||||
it('can correctly get one token', async () => {
|
||||
const so = getDefaultSO(canEncrypt);
|
||||
mockCreatePointInTimeFinderAsInternalUser([so]);
|
||||
|
||||
it('can correctly getTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO(canEncrypt);
|
||||
const so2 = getDefaultSO2(canEncrypt);
|
||||
const token = await uninstallTokenService.getToken(so.id);
|
||||
|
||||
const tokensMap = await uninstallTokenService.getTokensForPolicyIds([
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
expect(tokensMap).toEqual([
|
||||
{
|
||||
const expectedItem: UninstallToken = {
|
||||
id: so.id,
|
||||
policy_id: so.attributes.policy_id,
|
||||
token: getToken(so, canEncrypt),
|
||||
},
|
||||
{
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token: getToken(so2, canEncrypt),
|
||||
created_at: aDayAgo,
|
||||
},
|
||||
] as UninstallToken[]);
|
||||
created_at: so.created_at,
|
||||
};
|
||||
|
||||
expect(token).toEqual(expectedItem);
|
||||
|
||||
expect(esoClientMock.createPointInTimeFinderDecryptedAsInternalUser).toHaveBeenCalledWith(
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
filter: `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.id: "${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}:${so.id}"`,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('can correctly getAllTokens', async () => {
|
||||
const so = getDefaultSO(canEncrypt);
|
||||
const so2 = getDefaultSO2(canEncrypt);
|
||||
describe('getTokenMetadata', () => {
|
||||
it('can correctly get token metadata', async () => {
|
||||
const so = getDefaultSO(canEncrypt);
|
||||
const so2 = getDefaultSO2(canEncrypt);
|
||||
|
||||
const tokensMap = (await uninstallTokenService.getAllTokens()).items;
|
||||
expect(tokensMap).toEqual([
|
||||
{
|
||||
policy_id: so.attributes.policy_id,
|
||||
token: getToken(so, canEncrypt),
|
||||
},
|
||||
{
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token: getToken(so2, canEncrypt),
|
||||
created_at: aDayAgo,
|
||||
},
|
||||
] as UninstallToken[]);
|
||||
const actualItems = (await uninstallTokenService.getTokenMetadata()).items;
|
||||
const expectedItems: UninstallTokenMetadata[] = [
|
||||
{
|
||||
id: so.id,
|
||||
policy_id: so.attributes.policy_id,
|
||||
created_at: so.created_at,
|
||||
},
|
||||
{
|
||||
id: so2.id,
|
||||
policy_id: so2.attributes.policy_id,
|
||||
created_at: so2.created_at,
|
||||
},
|
||||
];
|
||||
expect(actualItems).toEqual(expectedItems);
|
||||
});
|
||||
|
||||
it('should throw error if created_at is missing', async () => {
|
||||
const defaultBuckets = getDefaultBuckets(canEncrypt);
|
||||
defaultBuckets[0].latest.hits.hits[0]._source.created_at = '';
|
||||
mockCreatePointInTimeFinder(canEncrypt, defaultBuckets);
|
||||
|
||||
await expect(uninstallTokenService.getTokenMetadata()).rejects.toThrowError(
|
||||
'Uninstall Token is missing creation date.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if policy_id is missing', async () => {
|
||||
const defaultBuckets = getDefaultBuckets(canEncrypt);
|
||||
defaultBuckets[0].latest.hits.hits[0]._source[
|
||||
UNINSTALL_TOKENS_SAVED_OBJECT_TYPE
|
||||
].policy_id = '';
|
||||
mockCreatePointInTimeFinder(canEncrypt, defaultBuckets);
|
||||
|
||||
await expect(uninstallTokenService.getTokenMetadata()).rejects.toThrowError(
|
||||
'Uninstall Token is missing policy ID.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import type {
|
|||
import type {
|
||||
AggregationsMultiBucketAggregateBase,
|
||||
AggregationsTopHitsAggregate,
|
||||
SearchHit,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
|
@ -26,9 +27,14 @@ import { asyncForEach } from '@kbn/std';
|
|||
|
||||
import type { AggregationsTermsInclude } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
import type { GetUninstallTokensResponse } from '../../../../common/types/rest_spec/uninstall_token';
|
||||
import { UninstallTokenError } from '../../../../common/errors';
|
||||
|
||||
import type { UninstallToken } from '../../../../common/types/models/uninstall_token';
|
||||
import type { GetUninstallTokensMetadataResponse } from '../../../../common/types/rest_spec/uninstall_token';
|
||||
|
||||
import type {
|
||||
UninstallToken,
|
||||
UninstallTokenMetadata,
|
||||
} from '../../../../common/types/models/uninstall_token';
|
||||
|
||||
import { UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../../../constants';
|
||||
import { appContextService } from '../../app_context';
|
||||
|
@ -50,30 +56,83 @@ interface UninstallTokenSOAggregation {
|
|||
}
|
||||
|
||||
export interface UninstallTokenServiceInterface {
|
||||
getTokenForPolicyId(policyId: string): Promise<UninstallToken | null>;
|
||||
/**
|
||||
* Get uninstall token based on its id.
|
||||
*
|
||||
* @param id
|
||||
* @returns uninstall token if found, null if not found
|
||||
*/
|
||||
getToken(id: string): Promise<UninstallToken | null>;
|
||||
|
||||
getTokensForPolicyIds(policyIds: string[]): Promise<UninstallToken[]>;
|
||||
|
||||
findTokensForPartialPolicyId(
|
||||
searchString: string,
|
||||
/**
|
||||
* Get uninstall token metadata, optionally filtering by partial policyID, paginated
|
||||
*
|
||||
* @param policyIdFilter a string for partial matching the policyId
|
||||
* @param page
|
||||
* @param perPage
|
||||
* @returns Uninstall Tokens Metadata Response
|
||||
*/
|
||||
getTokenMetadata(
|
||||
policyIdFilter?: string,
|
||||
page?: number,
|
||||
perPage?: number
|
||||
): Promise<GetUninstallTokensResponse>;
|
||||
|
||||
getAllTokens(page?: number, perPage?: number): Promise<GetUninstallTokensResponse>;
|
||||
): Promise<GetUninstallTokensMetadataResponse>;
|
||||
|
||||
/**
|
||||
* Get hashed uninstall token for given policy id
|
||||
*
|
||||
* @param policyId agent policy id
|
||||
* @returns hashedToken
|
||||
*/
|
||||
getHashedTokenForPolicyId(policyId: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Get hashed uninstall tokens for given policy ids
|
||||
*
|
||||
* @param policyIds agent policy ids
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
getHashedTokensForPolicyIds(policyIds?: string[]): Promise<Record<string, string>>;
|
||||
|
||||
/**
|
||||
* Get hashed uninstall token for all policies
|
||||
*
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
getAllHashedTokens(): Promise<Record<string, string>>;
|
||||
|
||||
/**
|
||||
* Generate uninstall token for given policy id
|
||||
* Will not create a new token if one already exists for a given policy unless force: true is used
|
||||
*
|
||||
* @param policyId agent policy id
|
||||
* @param force generate a new token even if one already exists
|
||||
* @returns hashedToken
|
||||
*/
|
||||
generateTokenForPolicyId(policyId: string, force?: boolean): Promise<string>;
|
||||
|
||||
/**
|
||||
* Generate uninstall tokens for given policy ids
|
||||
* Will not create a new token if one already exists for a given policy unless force: true is used
|
||||
*
|
||||
* @param policyIds agent policy ids
|
||||
* @param force generate a new token even if one already exists
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
generateTokensForPolicyIds(policyIds: string[], force?: boolean): Promise<Record<string, string>>;
|
||||
|
||||
/**
|
||||
* Generate uninstall tokens all policies
|
||||
* Will not create a new token if one already exists for a given policy unless force: true is used
|
||||
*
|
||||
* @param force generate a new token even if one already exists
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
generateTokensForAllPolicies(force?: boolean): Promise<Record<string, string>>;
|
||||
|
||||
/**
|
||||
* If encryption is available, checks for any plain text uninstall tokens and encrypts them
|
||||
*/
|
||||
encryptTokens(): Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -82,62 +141,97 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
|
||||
constructor(private esoClient: EncryptedSavedObjectsClient) {}
|
||||
|
||||
/**
|
||||
* gets uninstall token for given policy id
|
||||
*
|
||||
* @param policyId agent policy id
|
||||
* @returns uninstall token if found
|
||||
*/
|
||||
public async getTokenForPolicyId(policyId: string): Promise<UninstallToken | null> {
|
||||
return (await this.getTokensByIncludeFilter({ include: policyId })).items[0] ?? null;
|
||||
public async getToken(id: string): Promise<UninstallToken | null> {
|
||||
const filter = `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.id: "${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}:${id}"`;
|
||||
|
||||
const uninstallTokens = await this.getDecryptedTokens({ filter });
|
||||
|
||||
return uninstallTokens.length === 1 ? uninstallTokens[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets uninstall tokens for given policy ids
|
||||
*
|
||||
* @param policyIds agent policy ids
|
||||
* @returns array of UninstallToken objects
|
||||
*/
|
||||
public async getTokensForPolicyIds(policyIds: string[]): Promise<UninstallToken[]> {
|
||||
return (await this.getTokensByIncludeFilter({ include: policyIds })).items;
|
||||
}
|
||||
/**
|
||||
* gets uninstall token for given policy id, paginated
|
||||
*
|
||||
* @param searchString a string for partial matching the policyId
|
||||
* @param page
|
||||
* @param perPage
|
||||
* @param policyId agent policy id
|
||||
* @returns GetUninstallTokensResponse
|
||||
*/
|
||||
public async findTokensForPartialPolicyId(
|
||||
searchString: string,
|
||||
page: number = 1,
|
||||
perPage: number = 20
|
||||
): Promise<GetUninstallTokensResponse> {
|
||||
return await this.getTokensByIncludeFilter({ include: `.*${searchString}.*`, page, perPage });
|
||||
}
|
||||
|
||||
/**
|
||||
* gets uninstall tokens for all policies, optionally paginated or returns all tokens
|
||||
* @param page
|
||||
* @param perPage
|
||||
* @returns GetUninstallTokensResponse
|
||||
*/
|
||||
public async getAllTokens(page?: number, perPage?: number): Promise<GetUninstallTokensResponse> {
|
||||
return this.getTokensByIncludeFilter({ perPage, page });
|
||||
}
|
||||
|
||||
private async getTokensByIncludeFilter({
|
||||
public async getTokenMetadata(
|
||||
policyIdFilter?: string,
|
||||
page = 1,
|
||||
perPage = SO_SEARCH_LIMIT,
|
||||
include,
|
||||
}: {
|
||||
include?: AggregationsTermsInclude;
|
||||
perPage?: number;
|
||||
page?: number;
|
||||
}): Promise<GetUninstallTokensResponse> {
|
||||
perPage = 20
|
||||
): Promise<GetUninstallTokensMetadataResponse> {
|
||||
const includeFilter = policyIdFilter ? `.*${policyIdFilter}.*` : undefined;
|
||||
|
||||
const tokenObjects = await this.getTokenObjectsByIncludeFilter(includeFilter);
|
||||
|
||||
const items: UninstallTokenMetadata[] = tokenObjects
|
||||
.slice((page - 1) * perPage, page * perPage)
|
||||
.map<UninstallTokenMetadata>(({ _id, _source }) => {
|
||||
this.assertPolicyId(_source[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE]);
|
||||
this.assertCreatedAt(_source.created_at);
|
||||
|
||||
return {
|
||||
id: _id.replace(`${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}:`, ''),
|
||||
policy_id: _source[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE].policy_id,
|
||||
created_at: _source.created_at,
|
||||
};
|
||||
});
|
||||
|
||||
return { items, total: tokenObjects.length, page, perPage };
|
||||
}
|
||||
|
||||
private async getDecryptedTokensForPolicyIds(policyIds: string[]): Promise<UninstallToken[]> {
|
||||
const tokenObjectHits = await this.getTokenObjectsByIncludeFilter(policyIds);
|
||||
|
||||
if (tokenObjectHits.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filter: string = tokenObjectHits
|
||||
.map(({ _id }) => {
|
||||
return `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.id: "${_id}"`;
|
||||
})
|
||||
.join(' or ');
|
||||
|
||||
return this.getDecryptedTokens({ filter });
|
||||
}
|
||||
|
||||
private getDecryptedTokens = async (
|
||||
options: Partial<SavedObjectsCreatePointInTimeFinderOptions>
|
||||
): Promise<UninstallToken[]> => {
|
||||
const tokensFinder =
|
||||
await this.esoClient.createPointInTimeFinderDecryptedAsInternalUser<UninstallTokenSOAttributes>(
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
...options,
|
||||
}
|
||||
);
|
||||
let tokenObject: Array<SavedObjectsFindResult<UninstallTokenSOAttributes>> = [];
|
||||
|
||||
for await (const result of tokensFinder.find()) {
|
||||
tokenObject = result.saved_objects;
|
||||
break;
|
||||
}
|
||||
tokensFinder.close();
|
||||
|
||||
const uninstallTokens: UninstallToken[] = tokenObject.map(
|
||||
({ id: _id, attributes, created_at: createdAt }) => {
|
||||
this.assertPolicyId(attributes);
|
||||
this.assertToken(attributes);
|
||||
this.assertCreatedAt(createdAt);
|
||||
|
||||
return {
|
||||
id: _id,
|
||||
policy_id: attributes.policy_id,
|
||||
token: attributes.token || attributes.token_plain,
|
||||
created_at: createdAt,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return uninstallTokens;
|
||||
};
|
||||
|
||||
private async getTokenObjectsByIncludeFilter(
|
||||
include?: AggregationsTermsInclude
|
||||
): Promise<Array<SearchHit<any>>> {
|
||||
const bucketSize = 10000;
|
||||
|
||||
const query: SavedObjectsCreatePointInTimeFinderOptions = {
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
|
@ -153,12 +247,14 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
top_hits: {
|
||||
size: 1,
|
||||
sort: [{ created_at: { order: 'desc' } }],
|
||||
_source: [`${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.policy_id`, 'created_at'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// encrypted saved objects doesn't decrypt aggregation values so we get
|
||||
// the ids first from saved objects to use with encrypted saved objects
|
||||
const idFinder = this.soClient.createPointInTimeFinder<
|
||||
|
@ -178,76 +274,22 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
break;
|
||||
}
|
||||
|
||||
const firstItemsIndexInPage = (page - 1) * perPage;
|
||||
const isCurrentPageEmpty = firstItemsIndexInPage >= aggResults.length;
|
||||
if (isCurrentPageEmpty) {
|
||||
return { items: [], total: aggResults.length, page, perPage };
|
||||
}
|
||||
|
||||
const getCreatedAt = (soBucket: UninstallTokenSOAggregationBucket) =>
|
||||
new Date(soBucket.latest.hits.hits[0]._source?.created_at ?? Date.now()).getTime();
|
||||
|
||||
// sort buckets by { created_at: 'desc' }
|
||||
// this is done with `slice()` instead of ES, because
|
||||
// 1) the query below doesn't support pagination, so we need to slice the IDs here,
|
||||
// 2) the query above doesn't support bucket sorting based on sub aggregation, see this:
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_ordering_by_a_sub_aggregation
|
||||
// sorting and paginating buckets is done here instead of ES,
|
||||
// because SO query doesn't support `bucket_sort`
|
||||
aggResults.sort((a, b) => getCreatedAt(b) - getCreatedAt(a));
|
||||
|
||||
const filter: string = aggResults
|
||||
.slice((page - 1) * perPage, page * perPage)
|
||||
.map(({ latest }) => {
|
||||
return `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.id: "${latest.hits.hits[0]._id}"`;
|
||||
})
|
||||
.join(' or ');
|
||||
|
||||
const tokensFinder =
|
||||
await this.esoClient.createPointInTimeFinderDecryptedAsInternalUser<UninstallTokenSOAttributes>(
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
filter,
|
||||
}
|
||||
);
|
||||
let tokenObjects: Array<SavedObjectsFindResult<UninstallTokenSOAttributes>> = [];
|
||||
|
||||
for await (const result of tokensFinder.find()) {
|
||||
tokenObjects = result.saved_objects;
|
||||
break;
|
||||
}
|
||||
tokensFinder.close();
|
||||
|
||||
const items: UninstallToken[] = tokenObjects
|
||||
.filter(
|
||||
({ attributes }) => attributes.policy_id && (attributes.token || attributes.token_plain)
|
||||
)
|
||||
.map(({ attributes, created_at: createdAt }) => ({
|
||||
policy_id: attributes.policy_id,
|
||||
token: attributes.token || attributes.token_plain,
|
||||
...(createdAt ? { created_at: createdAt } : {}),
|
||||
}));
|
||||
|
||||
return { items, total: aggResults.length, page, perPage };
|
||||
return aggResults.map((bucket) => bucket.latest.hits.hits[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* get hashed uninstall token for given policy id
|
||||
*
|
||||
* @param policyId agent policy id
|
||||
* @returns hashedToken
|
||||
*/
|
||||
public async getHashedTokenForPolicyId(policyId: string): Promise<string> {
|
||||
return (await this.getHashedTokensForPolicyIds([policyId]))[policyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* get hashed uninstall tokens for given policy ids
|
||||
*
|
||||
* @param policyIds agent policy ids
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
public async getHashedTokensForPolicyIds(policyIds: string[]): Promise<Record<string, string>> {
|
||||
const tokens = await this.getTokensForPolicyIds(policyIds);
|
||||
const tokens = await this.getDecryptedTokensForPolicyIds(policyIds);
|
||||
return tokens.reduce((acc, { policy_id: policyId, token }) => {
|
||||
if (policyId && token) {
|
||||
acc[policyId] = this.hashToken(token);
|
||||
|
@ -256,36 +298,15 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
}, {} as Record<string, string>);
|
||||
}
|
||||
|
||||
/**
|
||||
* get hashed uninstall token for all policies
|
||||
*
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
public async getAllHashedTokens(): Promise<Record<string, string>> {
|
||||
const policyIds = await this.getAllPolicyIds();
|
||||
return this.getHashedTokensForPolicyIds(policyIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* generate uninstall token for given policy id
|
||||
* will not create a new token if one already exists for a given policy unless force: true is used
|
||||
*
|
||||
* @param policyId agent policy id
|
||||
* @param force generate a new token even if one already exists
|
||||
* @returns hashedToken
|
||||
*/
|
||||
public async generateTokenForPolicyId(policyId: string, force: boolean = false): Promise<string> {
|
||||
return (await this.generateTokensForPolicyIds([policyId], force))[policyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* generate uninstall tokens for given policy ids
|
||||
* will not create a new token if one already exists for a given policy unless force: true is used
|
||||
*
|
||||
* @param policyIds agent policy ids
|
||||
* @param force generate a new token even if one already exists
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
public async generateTokensForPolicyIds(
|
||||
policyIds: string[],
|
||||
force: boolean = false
|
||||
|
@ -298,7 +319,7 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
|
||||
const existingTokens = force
|
||||
? {}
|
||||
: (await this.getTokensForPolicyIds(policyIds)).reduce(
|
||||
: (await this.getDecryptedTokensForPolicyIds(policyIds)).reduce(
|
||||
(acc, { policy_id: policyId, token }) => {
|
||||
acc[policyId] = token;
|
||||
return acc;
|
||||
|
@ -339,13 +360,6 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
}, {} as Record<string, string>);
|
||||
}
|
||||
|
||||
/**
|
||||
* generate uninstall tokens all policies
|
||||
* will not create a new token if one already exists for a given policy unless force: true is used
|
||||
*
|
||||
* @param force generate a new token even if one already exists
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
public async generateTokensForAllPolicies(
|
||||
force: boolean = false
|
||||
): Promise<Record<string, string>> {
|
||||
|
@ -353,9 +367,6 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
return this.generateTokensForPolicyIds(policyIds, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* if encryption is available, checks for any plain text uninstall tokens and encrypts them
|
||||
*/
|
||||
public async encryptTokens(): Promise<void> {
|
||||
if (!this.isEncryptionAvailable) {
|
||||
return;
|
||||
|
@ -477,4 +488,22 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
|
|||
private get isEncryptionAvailable(): boolean {
|
||||
return appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt ?? false;
|
||||
}
|
||||
|
||||
private assertCreatedAt(createdAt: string | undefined): asserts createdAt is string {
|
||||
if (!createdAt) {
|
||||
throw new UninstallTokenError('Uninstall Token is missing creation date.');
|
||||
}
|
||||
}
|
||||
|
||||
private assertToken(attributes: UninstallTokenSOAttributes | undefined) {
|
||||
if (!attributes?.token && !attributes?.token_plain) {
|
||||
throw new UninstallTokenError('Uninstall Token is missing the token.');
|
||||
}
|
||||
}
|
||||
|
||||
private assertPolicyId(attributes: UninstallTokenSOAttributes | undefined) {
|
||||
if (!attributes?.policy_id) {
|
||||
throw new UninstallTokenError('Uninstall Token is missing policy ID.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,16 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const GetUninstallTokensRequestSchema = {
|
||||
export const GetUninstallTokensMetadataRequestSchema = {
|
||||
query: schema.object({
|
||||
policyId: schema.maybe(schema.string()),
|
||||
perPage: schema.maybe(schema.number({ defaultValue: 20, min: 5 })),
|
||||
page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })),
|
||||
}),
|
||||
};
|
||||
|
||||
export const GetUninstallTokenRequestSchema = {
|
||||
params: schema.object({
|
||||
uninstallTokenId: schema.string(),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -7,15 +7,13 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import {
|
||||
AGENT_POLICY_API_ROUTES,
|
||||
CreateAgentPolicyResponse,
|
||||
UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
} from '@kbn/fleet-plugin/common';
|
||||
import { UNINSTALL_TOKEN_ROUTES } from '@kbn/fleet-plugin/common/constants';
|
||||
import { GetUninstallTokensResponse } from '@kbn/fleet-plugin/common/types/rest_spec/uninstall_token';
|
||||
import * as uuid from 'uuid';
|
||||
GetUninstallTokensMetadataResponse,
|
||||
GetUninstallTokenResponse,
|
||||
} from '@kbn/fleet-plugin/common/types/rest_spec/uninstall_token';
|
||||
import { uninstallTokensRouteService } from '@kbn/fleet-plugin/common/services';
|
||||
import { testUsers } from '../test_users';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { addUninstallTokenToPolicy, generateNPolicies } from '../../helpers';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
|
@ -24,202 +22,312 @@ export default function (providerContext: FtrProviderContext) {
|
|||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('Uninstall Token API', () => {
|
||||
let generatedPolicyIds: Set<string>;
|
||||
|
||||
before(async () => {
|
||||
await cleanSavedObjects();
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cleanSavedObjects();
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
before(async () => {
|
||||
generatedPolicyIds = new Set(await generatePolicies(20));
|
||||
});
|
||||
describe('GET uninstall_tokens', () => {
|
||||
describe('pagination', () => {
|
||||
let generatedPolicyIds: Set<string>;
|
||||
|
||||
after(async () => {
|
||||
await cleanSavedObjects();
|
||||
});
|
||||
before(async () => {
|
||||
generatedPolicyIds = new Set(await generateNPolicies(supertest, 20));
|
||||
});
|
||||
|
||||
it('should return tokens for all policies if number of policies is below default perPage', async () => {
|
||||
const response = await supertest.get(UNINSTALL_TOKEN_ROUTES.LIST_PATTERN).expect(200);
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
const body: GetUninstallTokensResponse = response.body;
|
||||
expect(body.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
|
||||
expect(body.items.length).to.equal(generatedPolicyIds.size);
|
||||
body.items.forEach(({ policy_id: policyId }) =>
|
||||
expect(generatedPolicyIds.has(policyId)).to.be(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return token with creation date', async () => {
|
||||
const response = await supertest.get(UNINSTALL_TOKEN_ROUTES.LIST_PATTERN).expect(200);
|
||||
|
||||
const body: GetUninstallTokensResponse = response.body;
|
||||
expect(body.items[0]).to.have.property('token');
|
||||
expect(body.items[0]).to.have.property('created_at');
|
||||
|
||||
const createdAt = new Date(body.items[0].created_at!).getTime();
|
||||
const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000).getTime();
|
||||
|
||||
expect(createdAt).to.lessThan(Date.now()).greaterThan(thirtyMinutesAgo);
|
||||
});
|
||||
|
||||
it('should return default perPage number of tokens if total is above default perPage', async () => {
|
||||
generatedPolicyIds.add((await generatePolicies(1))[0]);
|
||||
|
||||
const response1 = await supertest.get(UNINSTALL_TOKEN_ROUTES.LIST_PATTERN).expect(200);
|
||||
const body1: GetUninstallTokensResponse = response1.body;
|
||||
expect(body1.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body1.page).to.equal(1);
|
||||
expect(body1.perPage).to.equal(20);
|
||||
expect(body1.items.length).to.equal(20);
|
||||
|
||||
const response2 = await supertest
|
||||
.get(UNINSTALL_TOKEN_ROUTES.LIST_PATTERN)
|
||||
.query({ page: 2 })
|
||||
.expect(200);
|
||||
const body2: GetUninstallTokensResponse = response2.body;
|
||||
expect(body2.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body2.page).to.equal(2);
|
||||
expect(body2.perPage).to.equal(20);
|
||||
expect(body2.items.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('should return all tokens via pagination', async () => {
|
||||
const receivedPolicyIds: string[] = [];
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
it('should return token metadata for all policies if number of policies is below default perPage', async () => {
|
||||
const response = await supertest
|
||||
.get(UNINSTALL_TOKEN_ROUTES.LIST_PATTERN)
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
|
||||
expect(body.items.length).to.equal(generatedPolicyIds.size);
|
||||
body.items.forEach(({ policy_id: policyId }) =>
|
||||
expect(generatedPolicyIds.has(policyId)).to.be(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return token metadata with creation date and id', 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('created_at');
|
||||
expect(body.items[0]).to.have.property('id');
|
||||
|
||||
const createdAt = new Date(body.items[0].created_at!).getTime();
|
||||
const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000).getTime();
|
||||
|
||||
expect(createdAt).to.lessThan(Date.now()).greaterThan(thirtyMinutesAgo);
|
||||
});
|
||||
|
||||
it('should return default perPage number of token metadata if total is above default perPage', async () => {
|
||||
generatedPolicyIds.add((await generateNPolicies(supertest, 1))[0]);
|
||||
|
||||
const response1 = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.expect(200);
|
||||
const body1: GetUninstallTokensMetadataResponse = response1.body;
|
||||
expect(body1.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body1.page).to.equal(1);
|
||||
expect(body1.perPage).to.equal(20);
|
||||
expect(body1.items.length).to.equal(20);
|
||||
|
||||
const response2 = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({ page: 2 })
|
||||
.expect(200);
|
||||
const body2: GetUninstallTokensMetadataResponse = response2.body;
|
||||
expect(body2.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body2.page).to.equal(2);
|
||||
expect(body2.perPage).to.equal(20);
|
||||
expect(body2.items.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('should return metadata for all tokens via pagination', async () => {
|
||||
const receivedPolicyIds: string[] = [];
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
perPage: 8,
|
||||
page: i,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body.perPage).to.equal(8);
|
||||
expect(body.page).to.equal(i);
|
||||
|
||||
const receivedIds = body.items.map(({ policy_id: policyId }) => policyId);
|
||||
receivedPolicyIds.push(...receivedIds);
|
||||
}
|
||||
|
||||
expect(receivedPolicyIds.length).to.equal(generatedPolicyIds.size);
|
||||
receivedPolicyIds.forEach((policyId) =>
|
||||
expect(generatedPolicyIds.has(policyId)).to.be(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return token metadata correctly paginated and sorted by their creation date desc', async () => {
|
||||
let prevCreatedAt = Date.now();
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
perPage: 6,
|
||||
page: i,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
|
||||
body.items.forEach(({ created_at: createdAt }) => {
|
||||
const currentCreatedAt = new Date(createdAt!).getTime();
|
||||
|
||||
const isCurrentOlderThanPrevious = currentCreatedAt <= prevCreatedAt;
|
||||
expect(isCurrentOlderThanPrevious).to.be(true);
|
||||
|
||||
prevCreatedAt = currentCreatedAt;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are multiple tokens for a policy', () => {
|
||||
let generatedPolicyIdsArray: string[];
|
||||
let timestampBeforeAddingNewTokens: number;
|
||||
|
||||
before(async () => {
|
||||
generatedPolicyIdsArray = await generateNPolicies(supertest, 20);
|
||||
|
||||
timestampBeforeAddingNewTokens = Date.now();
|
||||
|
||||
const savingAdditionalTokensPromises = generatedPolicyIdsArray.map((id) =>
|
||||
addUninstallTokenToPolicy(kibanaServer, id, `${id} latest token`)
|
||||
);
|
||||
|
||||
await Promise.all(savingAdditionalTokensPromises);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it("should return only the latest token's metadata for every policy", async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensMetadataResponse = response.body;
|
||||
expect(body.total).to.equal(generatedPolicyIdsArray.length);
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.equal(20);
|
||||
|
||||
body.items.forEach((uninstallToken) => {
|
||||
const createdAt = new Date(uninstallToken.created_at!).getTime();
|
||||
expect(createdAt).to.be.greaterThan(timestampBeforeAddingNewTokens);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `policyId` query param is used', () => {
|
||||
let generatedPolicyIdsArray: string[];
|
||||
|
||||
before(async () => {
|
||||
generatedPolicyIdsArray = await generateNPolicies(supertest, 5);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('should return token metadata for full policyID if found', async () => {
|
||||
const selectedPolicyId = generatedPolicyIdsArray[3];
|
||||
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
perPage: 8,
|
||||
page: i,
|
||||
policyId: selectedPolicyId,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensResponse = response.body;
|
||||
expect(body.total).to.equal(generatedPolicyIds.size);
|
||||
expect(body.perPage).to.equal(8);
|
||||
expect(body.page).to.equal(i);
|
||||
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);
|
||||
});
|
||||
|
||||
const receivedIds = body.items.map(({ policy_id: policyId }) => policyId);
|
||||
receivedPolicyIds.push(...receivedIds);
|
||||
}
|
||||
it('should return token metadata for partial policyID if found', async () => {
|
||||
const selectedPolicyId = generatedPolicyIdsArray[2];
|
||||
|
||||
expect(receivedPolicyIds.length).to.equal(generatedPolicyIds.size);
|
||||
receivedPolicyIds.forEach((policyId) =>
|
||||
expect(generatedPolicyIds.has(policyId)).to.be(true)
|
||||
);
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
policyId: 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 nothing if policy is not found', async () => {
|
||||
const response = await supertest
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.query({
|
||||
policyId: 'not-existing-policy-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([]);
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
await supertestWithoutAuth
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.auth(username, password)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return 403 if the user does not have FLEET ALL privilege', async () => {
|
||||
const { username, password } = testUsers.fleet_no_access;
|
||||
|
||||
await supertestWithoutAuth
|
||||
.get(uninstallTokensRouteService.getListPath())
|
||||
.auth(username, password)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `policyId` query param is used', () => {
|
||||
describe('GET uninstall_tokens/{uninstallTokenId}', () => {
|
||||
let generatedUninstallTokenId: string;
|
||||
|
||||
before(async () => {
|
||||
generatedPolicyIds = new Set(await generatePolicies(5));
|
||||
generatedUninstallTokenId = await addUninstallTokenToPolicy(
|
||||
kibanaServer,
|
||||
'the policy id',
|
||||
'the token'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cleanSavedObjects();
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('should return token for full policyID if found', async () => {
|
||||
const selectedPolicyId = [...generatedPolicyIds][3];
|
||||
|
||||
it('should return decrypted token', async () => {
|
||||
const response = await supertest
|
||||
.get(UNINSTALL_TOKEN_ROUTES.LIST_PATTERN)
|
||||
.query({
|
||||
policyId: selectedPolicyId,
|
||||
})
|
||||
.get(uninstallTokensRouteService.getInfoPath(generatedUninstallTokenId))
|
||||
.expect(200);
|
||||
|
||||
const body: GetUninstallTokensResponse = 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);
|
||||
const body: GetUninstallTokenResponse = response.body;
|
||||
|
||||
expect(body.item.id).to.equal(generatedUninstallTokenId);
|
||||
expect(body.item.policy_id).to.equal('the policy id');
|
||||
expect(body.item.token).to.equal('the token');
|
||||
expect(body.item).to.have.property('created_at');
|
||||
});
|
||||
|
||||
it('should return token for partial policyID if found', async () => {
|
||||
const selectedPolicyId = [...generatedPolicyIds][2];
|
||||
|
||||
it('should return 404 if token is not found', async () => {
|
||||
const response = await supertest
|
||||
.get(UNINSTALL_TOKEN_ROUTES.LIST_PATTERN)
|
||||
.query({
|
||||
policyId: selectedPolicyId.slice(4, 11),
|
||||
})
|
||||
.expect(200);
|
||||
.get(uninstallTokensRouteService.getInfoPath('i-dont-exist'))
|
||||
.expect(404);
|
||||
|
||||
const body: GetUninstallTokensResponse = 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);
|
||||
expect(response.body).to.have.property('statusCode', 404);
|
||||
expect(response.body).to.have.property(
|
||||
'message',
|
||||
'Uninstall Token not found with id i-dont-exist'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return nothing if policy is not found', async () => {
|
||||
const response = await supertest
|
||||
.get(UNINSTALL_TOKEN_ROUTES.LIST_PATTERN)
|
||||
.query({
|
||||
policyId: 'not-existing-policy-id',
|
||||
})
|
||||
.expect(200);
|
||||
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;
|
||||
|
||||
const body: GetUninstallTokensResponse = 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([]);
|
||||
});
|
||||
});
|
||||
await supertestWithoutAuth
|
||||
.get(uninstallTokensRouteService.getInfoPath(generatedUninstallTokenId))
|
||||
.auth(username, password)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
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;
|
||||
it('should return 403 if the user does not have FLEET ALL privilege', async () => {
|
||||
const { username, password } = testUsers.fleet_no_access;
|
||||
|
||||
await supertestWithoutAuth
|
||||
.get(UNINSTALL_TOKEN_ROUTES.LIST_PATTERN)
|
||||
.auth(username, password)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return 403 if the user does not have FLEET ALL privilege', async () => {
|
||||
const { username, password } = testUsers.fleet_no_access;
|
||||
|
||||
await supertestWithoutAuth
|
||||
.get(UNINSTALL_TOKEN_ROUTES.LIST_PATTERN)
|
||||
.auth(username, password)
|
||||
.expect(403);
|
||||
await supertestWithoutAuth
|
||||
.get(uninstallTokensRouteService.getInfoPath(generatedUninstallTokenId))
|
||||
.auth(username, password)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const generatePolicies = async (number: number) => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < number; i++) {
|
||||
promises.push(
|
||||
supertest
|
||||
.post(AGENT_POLICY_API_ROUTES.CREATE_PATTERN)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ name: `Agent Policy ${uuid.v4()}`, namespace: 'default' })
|
||||
.expect(200)
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
const policyIds = responses.map(({ body }) => (body as CreateAgentPolicyResponse).item.id);
|
||||
|
||||
return policyIds;
|
||||
};
|
||||
|
||||
const cleanSavedObjects = async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.savedObjects.clean({ types: [UNINSTALL_TOKENS_SAVED_OBJECT_TYPE] });
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
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 { KbnClient } from '@kbn/test';
|
||||
import { UNINSTALL_TOKENS_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
|
||||
import { FtrProviderContext } from '../api_integration/ftr_provider_context';
|
||||
|
||||
export function warnAndSkipTest(mochaContext: Mocha.Context, log: ToolingLog) {
|
||||
|
@ -114,3 +119,39 @@ export function setPrereleaseSetting(supertest: any) {
|
|||
.send({ prerelease_integrations_enabled: false });
|
||||
});
|
||||
}
|
||||
|
||||
export const generateNPolicies = async (supertest: any, number: number) => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < number; i++) {
|
||||
promises.push(
|
||||
supertest
|
||||
.post(agentPolicyRouteService.getCreatePath())
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ name: `Agent Policy ${uuid.v4()}`, namespace: 'default' })
|
||||
.expect(200)
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
const policyIds = responses.map(({ body }) => (body as CreateAgentPolicyResponse).item.id);
|
||||
|
||||
return policyIds;
|
||||
};
|
||||
|
||||
export const addUninstallTokenToPolicy = async (
|
||||
kibanaServer: KbnClient,
|
||||
policyId: string,
|
||||
token: string
|
||||
) => {
|
||||
const savedObject = await kibanaServer.savedObjects.create({
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: policyId,
|
||||
token,
|
||||
},
|
||||
overwrite: false,
|
||||
});
|
||||
|
||||
return savedObject.id;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue