Skip to content

Commit

Permalink
[Fleet][Endpoint Security][Agent Tamper Protection][Uninstall tokens]…
Browse files Browse the repository at this point in the history
… Hide uninstall tokens for managed policies (elastic#172767)

## Summary

- [x] Hides uninstall tokens for managed policies (we still generate the
uninstall token)
- [x] Hides agent tamper protection switch from UI if policy is managed
- [x] API guard that prevents users from turning on tamper protection if
the policy is managed
- [x] Unit tests

# Screenshot


![managed-uninstall](https://github.com/elastic/kibana/assets/56409205/1c444255-c487-47bf-82d5-2271137cbd70)
  • Loading branch information
parkiino authored Jan 12, 2024
1 parent d34915b commit e9b6bc8
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('Agent policy advanced options content', () => {

const render = ({
isProtected = false,
isManaged = false,
policyId = 'agent-policy-1',
newAgentPolicy = false,
packagePolicy = [createPackagePolicyMock()],
Expand All @@ -54,6 +55,7 @@ describe('Agent policy advanced options content', () => {
...createAgentPolicyMock(),
package_policies: packagePolicy,
id: policyId,
is_managed: isManaged,
};
}

Expand Down Expand Up @@ -91,6 +93,16 @@ describe('Agent policy advanced options content', () => {
render();
expect(renderResult.queryByTestId('tamperProtectionSwitch')).not.toBeInTheDocument();
});
it('should be visible if policy is not managed/hosted', () => {
usePlatinumLicense();
render({ isManaged: false });
expect(renderResult.queryByTestId('tamperProtectionSwitch')).toBeInTheDocument();
});
it('should not be visible if policy is managed/hosted', () => {
usePlatinumLicense();
render({ isManaged: true });
expect(renderResult.queryByTestId('tamperProtectionSwitch')).not.toBeInTheDocument();
});
it('switched to true enables the uninstall command link', async () => {
usePlatinumLicense();
render({ isProtected: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
}}
/>
</EuiDescribedFormGroup>
{agentTamperProtectionEnabled && licenseService.isPlatinum() && (
{agentTamperProtectionEnabled && licenseService.isPlatinum() && !agentPolicy.is_managed && (
<EuiDescribedFormGroup
title={
<h4>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,20 @@ import type { FleetRequestHandlerContext } from '../..';

import type { MockedFleetAppContext } from '../../mocks';
import { createAppContextStartContractMock, xpackMocks } from '../../mocks';
import { appContextService } from '../../services';
import { agentPolicyService, appContextService } from '../../services';
import type {
GetUninstallTokenRequestSchema,
GetUninstallTokensMetadataRequestSchema,
} from '../../types/rest_spec/uninstall_token';

import { createAgentPolicyMock } from '../../../common/mocks';

import { registerRoutes } from '.';

import { getUninstallTokenHandler, getUninstallTokensMetadataHandler } from './handlers';

jest.mock('../../services/agent_policy');

describe('uninstall token handlers', () => {
let context: FleetRequestHandlerContext;
let response: ReturnType<typeof httpServerMock.createResponseFactory>;
Expand Down Expand Up @@ -74,10 +78,17 @@ describe('uninstall token handlers', () => {
unknown,
TypeOf<typeof GetUninstallTokensMetadataRequestSchema.query>
>;
const mockAgentPolicyService = agentPolicyService as jest.Mocked<typeof agentPolicyService>;

beforeEach(() => {
const uninstallTokenService = appContextService.getUninstallTokenService()!;
getTokenMetadataMock = uninstallTokenService.getTokenMetadata as jest.Mock;
mockAgentPolicyService.list.mockResolvedValue({
items: [createAgentPolicyMock()],
total: 1,
page: 1,
perPage: 1,
});

request = httpServerMock.createKibanaRequest();
});
Expand Down
16 changes: 14 additions & 2 deletions x-pack/plugins/fleet/server/routes/uninstall_token/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
import type { TypeOf } from '@kbn/config-schema';
import type { CustomHttpResponseOptions, ResponseError } from '@kbn/core-http-server';

import { appContextService } from '../../services';
import { appContextService, agentPolicyService } from '../../services';
import type { FleetRequestHandler } from '../../types';
import type {
GetUninstallTokensMetadataRequestSchema,
GetUninstallTokenRequestSchema,
} from '../../types/rest_spec/uninstall_token';
import { defaultFleetErrorHandler } from '../../errors';
import type { GetUninstallTokenResponse } from '../../../common/types/rest_spec/uninstall_token';
import { AGENT_POLICY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../../constants';

const UNINSTALL_TOKEN_SERVICE_UNAVAILABLE_ERROR: CustomHttpResponseOptions<ResponseError> = {
statusCode: 500,
Expand All @@ -32,13 +33,24 @@ export const getUninstallTokensMetadataHandler: FleetRequestHandler<
}

try {
const fleetContext = await context.fleet;
const soClient = fleetContext.internalSoClient;

const { items: managedPolicies } = await agentPolicyService.list(soClient, {
fields: ['id'],
perPage: SO_SEARCH_LIMIT,
kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.is_managed:true`,
});

const managedPolicyIds = managedPolicies.map((policy) => policy.id);

const { page = 1, perPage = 20, policyId } = request.query;

const body = await uninstallTokenService.getTokenMetadata(
policyId?.trim(),
page,
perPage,
'policy-elastic-agent-on-cloud'
managedPolicyIds.length > 0 ? managedPolicyIds : undefined
);

return response.ok({ body });
Expand Down
27 changes: 26 additions & 1 deletion x-pack/plugins/fleet/server/services/agent_policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import { securityMock } from '@kbn/security-plugin/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import type { Logger } from '@kbn/core/server';

import { PackagePolicyRestrictionRelatedError, FleetUnauthorizedError } from '../errors';
import {
PackagePolicyRestrictionRelatedError,
FleetUnauthorizedError,
HostedAgentPolicyRestrictionRelatedError,
} from '../errors';
import type {
AgentPolicy,
FullAgentPolicy,
Expand Down Expand Up @@ -603,6 +607,27 @@ describe('agent policy', () => {
expect(calledWith[2]).toHaveProperty('is_managed', true);
});

it('should throw a HostedAgentRestrictionRelated error if user enables "is_protected" for a managed policy', async () => {
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;

soClient.get.mockResolvedValue({
attributes: { is_managed: true },
id: 'mocked',
type: 'mocked',
references: [],
});

await expect(
agentPolicyService.update(soClient, esClient, 'test-id', {
is_protected: true,
})
).rejects.toThrowError(
new HostedAgentPolicyRestrictionRelatedError('Cannot update is_protected')
);
});

it('should call audit logger', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,14 @@ export interface UninstallTokenServiceInterface {
* @param policyIdFilter a string for partial matching the policyId
* @param page
* @param perPage
* @param policyIdExcludeFilter
* @param excludePolicyIds
* @returns Uninstall Tokens Metadata Response
*/
getTokenMetadata(
policyIdFilter?: string,
page?: number,
perPage?: number,
policyIdExcludeFilter?: string
excludePolicyIds?: string[]
): Promise<GetUninstallTokensMetadataResponse>;

/**
Expand Down Expand Up @@ -176,14 +176,11 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
policyIdFilter?: string,
page = 1,
perPage = 20,
policyIdExcludeFilter?: string
excludePolicyIds?: string[]
): Promise<GetUninstallTokensMetadataResponse> {
const includeFilter = policyIdFilter ? `.*${policyIdFilter}.*` : undefined;

const tokenObjects = await this.getTokenObjectsByIncludeFilter(
includeFilter,
policyIdExcludeFilter
);
const tokenObjects = await this.getTokenObjectsByIncludeFilter(includeFilter, excludePolicyIds);

const items: UninstallTokenMetadata[] = tokenObjects
.slice((page - 1) * perPage, page * perPage)
Expand Down

0 comments on commit e9b6bc8

Please sign in to comment.