mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Fleet][Agent Tamper Protection] License watcher for agent policy (#160463)
## Summary
- [x] Adds an agent policy watcher to check if an agent policy is valid
for a specific license. If it is not valid, it will set the
platinum-only features (as of now, only tamper protection) to false.
- [x] Moves `generateNewAgentPolicyWithDefaults` up to `common/services`
- [x] Unit Tests
# Screenshot

---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c9dc2bf308
commit
40d2f685d6
15 changed files with 431 additions and 14 deletions
|
@ -5,9 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PostDeletePackagePoliciesResponse, NewPackagePolicy, PackagePolicy } from './types';
|
||||
import type {
|
||||
PostDeletePackagePoliciesResponse,
|
||||
NewPackagePolicy,
|
||||
PackagePolicy,
|
||||
AgentPolicy,
|
||||
} from './types';
|
||||
import type { FleetAuthz } from './authz';
|
||||
import { ENDPOINT_PRIVILEGES } from './constants';
|
||||
import { dataTypes, ENDPOINT_PRIVILEGES } from './constants';
|
||||
|
||||
export const createNewPackagePolicyMock = (): NewPackagePolicy => {
|
||||
return {
|
||||
|
@ -105,3 +110,21 @@ export const createFleetAuthzMock = (): FleetAuthz => {
|
|||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createAgentPolicyMock = (overrideProps?: Partial<AgentPolicy>): AgentPolicy => {
|
||||
return {
|
||||
id: 'agent-policy-1',
|
||||
name: 'agent-policy-1',
|
||||
description: 'an agent policy',
|
||||
status: 'active',
|
||||
namespace: 'default',
|
||||
monitoring_enabled: Object.values(dataTypes),
|
||||
inactivity_timeout: 1209600,
|
||||
updated_at: '2023-06-30T16:03:38.159292',
|
||||
updated_by: 'user-1',
|
||||
revision: 1,
|
||||
is_managed: false,
|
||||
is_protected: false,
|
||||
...overrideProps,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { pick } from 'lodash';
|
||||
|
||||
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
|
||||
|
||||
import {
|
||||
isAgentPolicyValidForLicense,
|
||||
unsetAgentPolicyAccordingToLicenseLevel,
|
||||
} from './agent_policy_config';
|
||||
import { generateNewAgentPolicyWithDefaults } from './generate_new_agent_policy';
|
||||
|
||||
describe('agent policy config and licenses', () => {
|
||||
const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } });
|
||||
const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } });
|
||||
describe('isAgentPolicyValidForLicense', () => {
|
||||
it('does not allow agent tampering protection to be turned on for gold and below licenses', () => {
|
||||
const partialPolicy = { is_protected: true };
|
||||
expect(isAgentPolicyValidForLicense(partialPolicy, Gold)).toBeFalsy();
|
||||
});
|
||||
it('allows agent tampering protection to be turned on for platinum licenses', () => {
|
||||
const partialPolicy = { is_protected: true };
|
||||
expect(isAgentPolicyValidForLicense(partialPolicy, Platinum)).toBeTruthy();
|
||||
});
|
||||
it('allows agent tampering protection to be turned off for platinum licenses', () => {
|
||||
const partialPolicy = { is_protected: false };
|
||||
expect(isAgentPolicyValidForLicense(partialPolicy, Platinum)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('unsetAgentPolicyAccordingToLicenseLevel', () => {
|
||||
it('resets all paid features to default if license is gold', () => {
|
||||
const defaults = pick(generateNewAgentPolicyWithDefaults(), 'is_protected');
|
||||
const partialPolicy = { is_protected: true };
|
||||
const retPolicy = unsetAgentPolicyAccordingToLicenseLevel(partialPolicy, Gold);
|
||||
expect(retPolicy).toEqual(defaults);
|
||||
});
|
||||
it('does not change paid features if license is platinum', () => {
|
||||
const expected = pick(generateNewAgentPolicyWithDefaults(), 'is_protected');
|
||||
const partialPolicy = { is_protected: false };
|
||||
const expected2 = { is_protected: true };
|
||||
const partialPolicy2 = { is_protected: true };
|
||||
const retPolicy = unsetAgentPolicyAccordingToLicenseLevel(partialPolicy, Platinum);
|
||||
expect(retPolicy).toEqual(expected);
|
||||
const retPolicy2 = unsetAgentPolicyAccordingToLicenseLevel(partialPolicy2, Platinum);
|
||||
expect(retPolicy2).toEqual(expected2);
|
||||
});
|
||||
});
|
||||
});
|
54
x-pack/plugins/fleet/common/services/agent_policy_config.ts
Normal file
54
x-pack/plugins/fleet/common/services/agent_policy_config.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 type { ILicense } from '@kbn/licensing-plugin/common/types';
|
||||
|
||||
import type { AgentPolicy } from '../types';
|
||||
|
||||
import { agentPolicyWithoutPaidFeatures } from './generate_new_agent_policy';
|
||||
|
||||
function isAgentTamperingPolicyValidForLicense(
|
||||
policy: Partial<AgentPolicy>,
|
||||
license: ILicense | null
|
||||
) {
|
||||
if (license && license.hasAtLeast('platinum')) {
|
||||
// platinum allows agent tamper protection
|
||||
return true;
|
||||
}
|
||||
|
||||
const defaults = agentPolicyWithoutPaidFeatures(policy);
|
||||
|
||||
// only platinum or higher may modify agent tampering
|
||||
if (policy.is_protected !== defaults.is_protected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const isAgentPolicyValidForLicense = (
|
||||
policy: Partial<AgentPolicy>,
|
||||
license: ILicense | null
|
||||
): boolean => {
|
||||
return isAgentTamperingPolicyValidForLicense(policy, license);
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets paid features in a AgentPolicy back to default values
|
||||
* when unsupported by the given license level.
|
||||
*/
|
||||
export const unsetAgentPolicyAccordingToLicenseLevel = (
|
||||
policy: Partial<AgentPolicy>,
|
||||
license: ILicense | null
|
||||
): Partial<AgentPolicy> => {
|
||||
if (license && license.hasAtLeast('platinum')) {
|
||||
return policy;
|
||||
}
|
||||
|
||||
// set any license-gated features back to the defaults
|
||||
return agentPolicyWithoutPaidFeatures(policy);
|
||||
};
|
|
@ -17,6 +17,7 @@ describe('generateNewAgentPolicyWithDefaults', () => {
|
|||
namespace: 'default',
|
||||
monitoring_enabled: ['logs', 'metrics'],
|
||||
inactivity_timeout: 1209600,
|
||||
is_protected: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -26,6 +27,7 @@ describe('generateNewAgentPolicyWithDefaults', () => {
|
|||
description: 'test description',
|
||||
namespace: 'test-namespace',
|
||||
monitoring_enabled: ['logs'],
|
||||
is_protected: true,
|
||||
});
|
||||
|
||||
expect(newAgentPolicy).toEqual({
|
||||
|
@ -34,6 +36,7 @@ describe('generateNewAgentPolicyWithDefaults', () => {
|
|||
namespace: 'test-namespace',
|
||||
monitoring_enabled: ['logs'],
|
||||
inactivity_timeout: 1209600,
|
||||
is_protected: true,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,9 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { dataTypes } from '../../common/constants';
|
||||
import { dataTypes } from '../constants';
|
||||
|
||||
import type { NewAgentPolicy } from '../types';
|
||||
import type { AgentPolicy, NewAgentPolicy } from '../types';
|
||||
|
||||
const TWO_WEEKS_SECONDS = 1209600;
|
||||
// create a new agent policy with the defaults set
|
||||
|
@ -21,6 +21,16 @@ export function generateNewAgentPolicyWithDefaults(
|
|||
namespace: 'default',
|
||||
monitoring_enabled: Object.values(dataTypes),
|
||||
inactivity_timeout: TWO_WEEKS_SECONDS,
|
||||
is_protected: false,
|
||||
...overrideProps,
|
||||
};
|
||||
}
|
||||
|
||||
export function agentPolicyWithoutPaidFeatures(
|
||||
agentPolicy: Partial<AgentPolicy>
|
||||
): Partial<AgentPolicy> {
|
||||
return {
|
||||
...agentPolicy,
|
||||
is_protected: false,
|
||||
};
|
||||
}
|
|
@ -64,3 +64,13 @@ export { getAllowedOutputTypeForPolicy } from './output_helpers';
|
|||
export { agentStatusesToSummary } from './agent_statuses_to_summary';
|
||||
|
||||
export { policyHasFleetServer, policyHasAPMIntegration } from './agent_policies_helpers';
|
||||
|
||||
export {
|
||||
generateNewAgentPolicyWithDefaults,
|
||||
agentPolicyWithoutPaidFeatures,
|
||||
} from './generate_new_agent_policy';
|
||||
|
||||
export {
|
||||
isAgentPolicyValidForLicense,
|
||||
unsetAgentPolicyAccordingToLicenseLevel,
|
||||
} from './agent_policy_config';
|
||||
|
|
|
@ -22,11 +22,12 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { generateNewAgentPolicyWithDefaults } from '../../../services';
|
||||
import type { AgentPolicy, NewAgentPolicy } from '../../../types';
|
||||
|
||||
import { sendCreateAgentPolicy, useStartServices } from '../../../hooks';
|
||||
|
||||
import { generateNewAgentPolicyWithDefaults } from '../../../../../../common/services/generate_new_agent_policy';
|
||||
|
||||
import { agentPolicyFormValidation } from '.';
|
||||
|
||||
import { AgentPolicyAdvancedOptionsContent } from './agent_policy_advanced_fields';
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { generateNewAgentPolicyWithDefaults } from '../../../../../../../../common/services/generate_new_agent_policy';
|
||||
|
||||
import {
|
||||
sendCreateAgentPolicy,
|
||||
sendGetOneAgentPolicy,
|
||||
sendGetEnrollmentAPIKeys,
|
||||
} from '../../../../../../../hooks';
|
||||
|
||||
import { generateNewAgentPolicyWithDefaults } from '../../../../../../../services';
|
||||
|
||||
import type { AgentPolicy, NewAgentPolicy, EnrollmentAPIKey } from '../../../../../../../types';
|
||||
|
||||
interface UseGetAgentPolicyOrDefaultResponse {
|
||||
|
|
|
@ -401,6 +401,7 @@ describe('when on the package policy create page', () => {
|
|||
name: 'Agent policy 2',
|
||||
namespace: 'default',
|
||||
inactivity_timeout: 1209600,
|
||||
is_protected: false,
|
||||
},
|
||||
{ withSysMonitoring: false }
|
||||
);
|
||||
|
@ -432,6 +433,7 @@ describe('when on the package policy create page', () => {
|
|||
name: 'Agent policy 2',
|
||||
namespace: 'default',
|
||||
inactivity_timeout: 1209600,
|
||||
is_protected: false,
|
||||
},
|
||||
{ withSysMonitoring: true }
|
||||
);
|
||||
|
|
|
@ -30,7 +30,6 @@ import {
|
|||
import { useCancelAddPackagePolicy } from '../hooks';
|
||||
|
||||
import { splitPkgKey } from '../../../../../../../common/services';
|
||||
import { generateNewAgentPolicyWithDefaults } from '../../../../services';
|
||||
import type { NewAgentPolicy } from '../../../../types';
|
||||
import { useConfig, sendGetAgentStatus, useGetPackageInfoByKeyQuery } from '../../../../hooks';
|
||||
import {
|
||||
|
@ -56,6 +55,8 @@ import {
|
|||
StepSelectHosts,
|
||||
} from '../components';
|
||||
|
||||
import { generateNewAgentPolicyWithDefaults } from '../../../../../../../common/services/generate_new_agent_policy';
|
||||
|
||||
import { CreatePackagePolicySinglePageLayout, PostInstallAddAgentModal } from './components';
|
||||
import { useDevToolsRequest, useOnSubmit } from './hooks';
|
||||
import { PostInstallCloudFormationModal } from './components/post_install_cloud_formation_modal';
|
||||
|
|
|
@ -29,10 +29,8 @@ import { useAuthz, useStartServices, sendCreateAgentPolicy } from '../../../../h
|
|||
import { AgentPolicyForm, agentPolicyFormValidation } from '../../components';
|
||||
import { DevtoolsRequestFlyoutButton } from '../../../../components';
|
||||
import { generateCreateAgentPolicyDevToolsRequest } from '../../services';
|
||||
import {
|
||||
ExperimentalFeaturesService,
|
||||
generateNewAgentPolicyWithDefaults,
|
||||
} from '../../../../services';
|
||||
import { ExperimentalFeaturesService } from '../../../../services';
|
||||
import { generateNewAgentPolicyWithDefaults } from '../../../../../../../common/services/generate_new_agent_policy';
|
||||
|
||||
const FlyoutWithHigherZIndex = styled(EuiFlyout)`
|
||||
z-index: ${(props) => props.theme.eui.euiZLevel5};
|
||||
|
|
|
@ -48,6 +48,5 @@ export { isPackageUpdatable } from './is_package_updatable';
|
|||
export { pkgKeyFromPackageInfo } from './pkg_key_from_package_info';
|
||||
export { createExtensionRegistrationCallback } from './ui_extensions';
|
||||
export { incrementPolicyName } from './increment_policy_name';
|
||||
export { generateNewAgentPolicyWithDefaults } from './generate_new_agent_policy';
|
||||
export { getCloudFormationTemplateUrlFromPackagePolicy } from './get_cloud_formation_template_url_from_package_policy';
|
||||
export { getCloudFormationTemplateUrlFromAgentPolicy } from './get_cloud_formation_template_url_from_agent_policy';
|
||||
|
|
|
@ -127,6 +127,7 @@ import {
|
|||
} from './services/security/uninstall_token_service';
|
||||
import { FleetActionsClient, type FleetActionsClientInterface } from './services/actions';
|
||||
import type { FilesClientFactory } from './services/files/types';
|
||||
import { PolicyWatcher } from './services/agent_policy_watch';
|
||||
|
||||
export interface FleetSetupDeps {
|
||||
security: SecurityPluginSetup;
|
||||
|
@ -256,6 +257,7 @@ export class FleetPlugin
|
|||
private agentService?: AgentService;
|
||||
private packageService?: PackageService;
|
||||
private packagePolicyService?: PackagePolicyService;
|
||||
private policyWatcher?: PolicyWatcher;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config$ = this.initializerContext.config.create<FleetConfigType>();
|
||||
|
@ -491,7 +493,6 @@ export class FleetPlugin
|
|||
uninstallTokenService,
|
||||
});
|
||||
licenseService.start(plugins.licensing.license$);
|
||||
|
||||
this.telemetryEventsSender.start(plugins.telemetry, core);
|
||||
this.bulkActionsResolver?.start(plugins.taskManager);
|
||||
this.fleetUsageSender?.start(plugins.taskManager);
|
||||
|
@ -500,6 +501,10 @@ export class FleetPlugin
|
|||
|
||||
const logger = appContextService.getLogger();
|
||||
|
||||
this.policyWatcher = new PolicyWatcher(core.savedObjects, core.elasticsearch, logger);
|
||||
|
||||
this.policyWatcher.start(licenseService);
|
||||
|
||||
const fleetSetupPromise = (async () => {
|
||||
try {
|
||||
// Fleet remains `available` during setup as to excessively delay Kibana's boot process.
|
||||
|
@ -591,6 +596,7 @@ export class FleetPlugin
|
|||
|
||||
public async stop() {
|
||||
appContextService.stop();
|
||||
this.policyWatcher?.stop();
|
||||
licenseService.stop();
|
||||
this.telemetryEventsSender.stop();
|
||||
this.fleetStatus$.complete();
|
||||
|
|
114
x-pack/plugins/fleet/server/services/agent_policy_watch.test.ts
Normal file
114
x-pack/plugins/fleet/server/services/agent_policy_watch.test.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 {
|
||||
elasticsearchServiceMock,
|
||||
loggingSystemMock,
|
||||
savedObjectsServiceMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
|
||||
import type { ILicense } from '@kbn/licensing-plugin/common/types';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { LicenseService } from '../../common/services';
|
||||
|
||||
import { createAgentPolicyMock } from '../../common/mocks';
|
||||
|
||||
import { PolicyWatcher } from './agent_policy_watch';
|
||||
import { agentPolicyService } from './agent_policy';
|
||||
|
||||
jest.mock('./agent_policy');
|
||||
|
||||
const agentPolicySvcMock = agentPolicyService as jest.Mocked<typeof agentPolicyService>;
|
||||
|
||||
describe('Agent Policy-Changing license watcher', () => {
|
||||
const logger = loggingSystemMock.create().get('license_watch.test');
|
||||
const soStartMock = savedObjectsServiceMock.createStartContract();
|
||||
const esStartMock = elasticsearchServiceMock.createStart();
|
||||
|
||||
const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } });
|
||||
const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } });
|
||||
const Basic = licenseMock.createLicense({ license: { type: 'basic', mode: 'basic' } });
|
||||
|
||||
it('is activated on license changes', () => {
|
||||
// mock a license-changing service to test reactivity
|
||||
const licenseEmitter: Subject<ILicense> = new Subject();
|
||||
const licenseService = new LicenseService();
|
||||
const pw = new PolicyWatcher(soStartMock, esStartMock, logger);
|
||||
|
||||
// swap out watch function, just to ensure it gets called when a license change happens
|
||||
const mockWatch = jest.fn();
|
||||
pw.watch = mockWatch;
|
||||
|
||||
// licenseService is watching our subject for incoming licenses
|
||||
licenseService.start(licenseEmitter);
|
||||
pw.start(licenseService); // and the PolicyWatcher under test, uses that to subscribe as well
|
||||
|
||||
// Enqueue a license change!
|
||||
licenseEmitter.next(Platinum);
|
||||
|
||||
// policywatcher should have triggered
|
||||
expect(mockWatch.mock.calls.length).toBe(1);
|
||||
|
||||
pw.stop();
|
||||
licenseService.stop();
|
||||
licenseEmitter.complete();
|
||||
});
|
||||
|
||||
it('pages through all agent policies', async () => {
|
||||
const TOTAL = 247;
|
||||
|
||||
// set up the mocked agent policy service to return and do what we want
|
||||
agentPolicySvcMock.list
|
||||
.mockResolvedValueOnce({
|
||||
items: Array.from({ length: 100 }, () => createAgentPolicyMock()),
|
||||
total: TOTAL,
|
||||
page: 1,
|
||||
perPage: 100,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
items: Array.from({ length: 100 }, () => createAgentPolicyMock()),
|
||||
total: TOTAL,
|
||||
page: 2,
|
||||
perPage: 100,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
items: Array.from({ length: TOTAL - 200 }, () => createAgentPolicyMock()),
|
||||
total: TOTAL,
|
||||
page: 3,
|
||||
perPage: 100,
|
||||
});
|
||||
|
||||
const pw = new PolicyWatcher(soStartMock, esStartMock, logger);
|
||||
await pw.watch(Gold); // just manually trigger with a given license
|
||||
|
||||
expect(agentPolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts
|
||||
|
||||
// Assert: on the first call to agentPolicy.list, we asked for page 1
|
||||
expect(agentPolicySvcMock.list.mock.calls[0][1].page).toBe(1);
|
||||
expect(agentPolicySvcMock.list.mock.calls[1][1].page).toBe(2); // second call, asked for page 2
|
||||
expect(agentPolicySvcMock.list.mock.calls[2][1].page).toBe(3); // etc
|
||||
});
|
||||
|
||||
it('alters no-longer-licensed features', async () => {
|
||||
// mock an agent policy with agent tamper protection enabled
|
||||
agentPolicySvcMock.list.mockResolvedValueOnce({
|
||||
items: [createAgentPolicyMock({ is_protected: true })],
|
||||
total: 1,
|
||||
page: 1,
|
||||
perPage: 100,
|
||||
});
|
||||
|
||||
const pw = new PolicyWatcher(soStartMock, esStartMock, logger);
|
||||
|
||||
// emulate a license change below paid tier
|
||||
await pw.watch(Basic);
|
||||
|
||||
expect(agentPolicySvcMock.update).toHaveBeenCalled();
|
||||
expect(agentPolicySvcMock.update.mock.calls[0][3].is_protected).toEqual(false);
|
||||
});
|
||||
});
|
143
x-pack/plugins/fleet/server/services/agent_policy_watch.ts
Normal file
143
x-pack/plugins/fleet/server/services/agent_policy_watch.ts
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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 type { Subscription } from 'rxjs';
|
||||
import type {
|
||||
ElasticsearchClient,
|
||||
ElasticsearchServiceStart,
|
||||
KibanaRequest,
|
||||
Logger,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsServiceStart,
|
||||
} from '@kbn/core/server';
|
||||
import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
|
||||
import type { ILicense } from '@kbn/licensing-plugin/common/types';
|
||||
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import type { LicenseService } from '../../common/services/license';
|
||||
|
||||
import type { AgentPolicy } from '../../common';
|
||||
import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../common';
|
||||
import {
|
||||
isAgentPolicyValidForLicense,
|
||||
unsetAgentPolicyAccordingToLicenseLevel,
|
||||
} from '../../common/services/agent_policy_config';
|
||||
|
||||
import { agentPolicyService } from './agent_policy';
|
||||
|
||||
export class PolicyWatcher {
|
||||
private logger: Logger;
|
||||
private esClient: ElasticsearchClient;
|
||||
private subscription: Subscription | undefined;
|
||||
private soStart: SavedObjectsServiceStart;
|
||||
constructor(
|
||||
soStart: SavedObjectsServiceStart,
|
||||
esStart: ElasticsearchServiceStart,
|
||||
logger: Logger
|
||||
) {
|
||||
this.esClient = esStart.client.asInternalUser;
|
||||
this.logger = logger;
|
||||
this.soStart = soStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* The policy watcher is not called as part of a HTTP request chain, where the
|
||||
* request-scoped SOClient could be passed down. It is called via license observable
|
||||
* changes. We are acting as the 'system' in response to license changes, so we are
|
||||
* intentionally using the system user here. Be very aware of what you are using this
|
||||
* client to do
|
||||
*/
|
||||
private makeInternalSOClient(soStart: SavedObjectsServiceStart): SavedObjectsClientContract {
|
||||
const fakeRequest = {
|
||||
headers: {},
|
||||
getBasePath: () => '',
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: { href: {} },
|
||||
raw: { req: { url: '/' } },
|
||||
} as unknown as KibanaRequest;
|
||||
return soStart.getScopedClient(fakeRequest, { excludedExtensions: [SECURITY_EXTENSION_ID] });
|
||||
}
|
||||
|
||||
public start(licenseService: LicenseService) {
|
||||
this.subscription = licenseService.getLicenseInformation$()?.subscribe(this.watch.bind(this));
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
public async watch(license: ILicense) {
|
||||
let page = 1;
|
||||
let response: {
|
||||
items: AgentPolicy[];
|
||||
total: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
};
|
||||
|
||||
do {
|
||||
try {
|
||||
response = await agentPolicyService.list(this.makeInternalSOClient(this.soStart), {
|
||||
page: page++,
|
||||
perPage: 100,
|
||||
kuery: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.warn(
|
||||
`Unable to verify agent policies in line with license change: failed to fetch agent policies: ${e.message}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const updatedPolicyIds: string[] = [];
|
||||
for (const policy of response.items as AgentPolicy[]) {
|
||||
let updatePolicy = pick(policy, ['is_protected']) as Partial<AgentPolicy>;
|
||||
|
||||
try {
|
||||
if (!isAgentPolicyValidForLicense(updatePolicy, license)) {
|
||||
updatePolicy = unsetAgentPolicyAccordingToLicenseLevel(updatePolicy, license);
|
||||
try {
|
||||
this.logger.info('Updating agent policies per license change');
|
||||
await agentPolicyService.update(
|
||||
this.makeInternalSOClient(this.soStart),
|
||||
this.esClient,
|
||||
policy.id,
|
||||
updatePolicy
|
||||
);
|
||||
// accumulate list of policies updated
|
||||
updatedPolicyIds.push(policy.id);
|
||||
} catch (e) {
|
||||
// try again for transient issues
|
||||
try {
|
||||
await agentPolicyService.update(
|
||||
this.makeInternalSOClient(this.soStart),
|
||||
this.esClient,
|
||||
policy.id,
|
||||
updatePolicy
|
||||
);
|
||||
} catch (ee) {
|
||||
this.logger.warn(
|
||||
`Unable to remove platinum features from agent policy ${policy.id}`
|
||||
);
|
||||
this.logger.warn(ee);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failure while attempting to verify features for agent policy [${policy.id}]`
|
||||
);
|
||||
this.logger.warn(error);
|
||||
}
|
||||
}
|
||||
this.logger.info(`Agent policies updated by license change: [${updatedPolicyIds.join()}]`);
|
||||
} while (response.page * response.perPage < response.total);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue