Skip to content

Commit

Permalink
prevent delete flag to update from UI when preconfigured
Browse files Browse the repository at this point in the history
  • Loading branch information
juliaElastic committed Oct 10, 2024
1 parent 6fe1fc2 commit 147c2d4
Show file tree
Hide file tree
Showing 13 changed files with 303 additions and 35 deletions.
5 changes: 4 additions & 1 deletion x-pack/plugins/fleet/common/types/models/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@ export interface Settings extends BaseSettings {
output_secret_storage_requirements_met?: boolean;
use_space_awareness_migration_status?: 'pending' | 'success' | 'error';
use_space_awareness_migration_started_at?: string | null;
delete_unenrolled_agents?: boolean;
delete_unenrolled_agents?: {
enabled: boolean;
is_preconfigured: boolean;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
EuiSwitch,
EuiForm,
EuiFormRow,
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';

Expand All @@ -30,7 +31,10 @@ import {
export const AdvancedSection: React.FunctionComponent<{}> = ({}) => {
const authz = useAuthz();
const { docLinks, notifications } = useStartServices();
const deleteUnenrolledAgents = useGetSettings().data?.item?.delete_unenrolled_agents ?? false;
const deleteUnenrolledAgents =
useGetSettings().data?.item?.delete_unenrolled_agents?.enabled ?? false;
const isPreconfigured =
useGetSettings().data?.item?.delete_unenrolled_agents?.is_preconfigured ?? false;
const [deleteUnenrolledAgentsChecked, setDeleteUnenrolledAgentsChecked] =
React.useState<boolean>(deleteUnenrolledAgents);
const { mutateAsync: mutateSettingsAsync } = usePutSettingsMutation();
Expand All @@ -46,7 +50,10 @@ export const AdvancedSection: React.FunctionComponent<{}> = ({}) => {
try {
setDeleteUnenrolledAgentsChecked(deleteFlag);
const res = await mutateSettingsAsync({
delete_unenrolled_agents: deleteFlag,
delete_unenrolled_agents: {
enabled: deleteFlag,
is_preconfigured: false,
},
});

if (res.error) {
Expand Down Expand Up @@ -105,17 +112,27 @@ export const AdvancedSection: React.FunctionComponent<{}> = ({}) => {
}
>
<EuiFormRow label="">
<EuiSwitch
label={
<FormattedMessage
id="xpack.fleet.settings.deleteUnenrolledAgentsLabel"
defaultMessage="Delete unenrolled agents"
/>
<EuiToolTip
content={
isPreconfigured
? i18n.translate('xpack.fleet.settings.advancedSection.preconfiguredTitle', {
defaultMessage: 'This setting is preconfigured and cannot be updated.',
})
: undefined
}
checked={deleteUnenrolledAgentsChecked}
onChange={(e) => updateSettings(e.target.checked)}
disabled={!authz.fleet.allSettings}
/>
>
<EuiSwitch
label={
<FormattedMessage
id="xpack.fleet.settings.deleteUnenrolledAgentsLabel"
defaultMessage="Delete unenrolled agents"
/>
}
checked={deleteUnenrolledAgentsChecked}
onChange={(e) => updateSettings(e.target.checked)}
disabled={!authz.fleet.allSettings || isPreconfigured}
/>
</EuiToolTip>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiForm>
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class OutputUnauthorizedError extends FleetError {}
export class OutputInvalidError extends FleetError {}
export class OutputLicenceError extends FleetError {}
export class DownloadSourceError extends FleetError {}
export class DeleteUnenrolledAgentsPreconfiguredError extends FleetError {}

// Not found errors
export class AgentNotFoundError extends FleetNotFoundError {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ jest.mock('../../services', () => ({
has_seen_add_data_notice: true,
fleet_server_hosts: ['http://localhost:8220'],
prerelease_integrations_enabled: true,
delete_unenrolled_agents: true,
delete_unenrolled_agents: {
enabled: true,
is_preconfigured: false,
},
}),
},
appContextService: {
Expand Down Expand Up @@ -75,7 +78,10 @@ describe('SettingsHandler', () => {
has_seen_add_data_notice: true,
fleet_server_hosts: ['http://localhost:8220'],
prerelease_integrations_enabled: true,
delete_unenrolled_agents: true,
delete_unenrolled_agents: {
enabled: true,
is_preconfigured: false,
},
},
};
expect(response.ok).toHaveBeenCalledWith({
Expand Down
14 changes: 12 additions & 2 deletions x-pack/plugins/fleet/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,12 @@ export const getSavedObjectTypes = (
output_secret_storage_requirements_met: { type: 'boolean' },
use_space_awareness_migration_status: { type: 'keyword', index: false },
use_space_awareness_migration_started_at: { type: 'date', index: false },
delete_unenrolled_agents: { type: 'boolean', index: false },
delete_unenrolled_agents: {
properties: {
enabled: { type: 'boolean', index: false },
is_preconfigured: { type: 'boolean', index: false },
},
},
},
},
migrations: {
Expand All @@ -187,7 +192,12 @@ export const getSavedObjectTypes = (
{
type: 'mappings_addition',
addedMappings: {
delete_unenrolled_agents: { type: 'boolean', index: false },
delete_unenrolled_agents: {
properties: {
enabled: { type: 'boolean', index: false },
is_preconfigured: { type: 'boolean', index: false },
},
},
},
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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 { settingsService } from '..';

import { ensureDeleteUnenrolledAgentsSetting } from './delete_unenrolled_agent_setting';

jest.mock('..', () => ({
settingsService: {
getSettingsOrUndefined: jest.fn(),
saveSettings: jest.fn(),
},
}));

describe('delete_unenrolled_agent_setting', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should update settings with delete_unenrolled_agents enabled', async () => {
await ensureDeleteUnenrolledAgentsSetting({} as any, true);

expect(settingsService.saveSettings).toHaveBeenCalledWith(
expect.anything(),
{ delete_unenrolled_agents: { enabled: true, is_preconfigured: true } },
{ fromSetup: true }
);
});

it('should update settings with delete_unenrolled_agents disabled', async () => {
await ensureDeleteUnenrolledAgentsSetting({} as any, false);

expect(settingsService.saveSettings).toHaveBeenCalledWith(
expect.anything(),
{ delete_unenrolled_agents: { enabled: false, is_preconfigured: true } },
{ fromSetup: true }
);
});

it('should update settings when previously preconfigured', async () => {
(settingsService.getSettingsOrUndefined as jest.Mock).mockResolvedValue({
delete_unenrolled_agents: {
enabled: false,
is_preconfigured: true,
},
});
await ensureDeleteUnenrolledAgentsSetting({} as any);

expect(settingsService.saveSettings).toHaveBeenCalledWith(
expect.anything(),
{ delete_unenrolled_agents: { enabled: false, is_preconfigured: false } },
{ fromSetup: true }
);
});

it('should not update settings when previously not preconfigured', async () => {
(settingsService.getSettingsOrUndefined as jest.Mock).mockResolvedValue({
delete_unenrolled_agents: {
enabled: false,
is_preconfigured: false,
},
});
await ensureDeleteUnenrolledAgentsSetting({} as any);

expect(settingsService.saveSettings).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,30 @@ import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-ser
import { settingsService } from '..';
import type { FleetConfigType } from '../../config';

export function getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig(config?: FleetConfigType) {
return config.enableDeleteUnenrolledAgents ?? false;
export function getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig(
config?: FleetConfigType
): boolean | undefined {
return config.enableDeleteUnenrolledAgents;
}

export async function ensureDeleteUnenrolledAgentsSetting(
soClient: SavedObjectsClientContract,
enableDeleteUnenrolledAgents: boolean
enableDeleteUnenrolledAgents?: boolean
) {
if (enableDeleteUnenrolledAgents) {
await settingsService.saveSettings(soClient, {
delete_unenrolled_agents: true,
});
} else {
await settingsService.saveSettings(soClient, {
delete_unenrolled_agents: false,
});
if (enableDeleteUnenrolledAgents === undefined) {
const settings = await settingsService.getSettingsOrUndefined(soClient);
if (!settings?.delete_unenrolled_agents?.is_preconfigured) {
return;
}
}
await settingsService.saveSettings(
soClient,
{
delete_unenrolled_agents: {
enabled: !!enableDeleteUnenrolledAgents,
is_preconfigured: enableDeleteUnenrolledAgents !== undefined,
},
},
{ fromSetup: true }
);
}
117 changes: 117 additions & 0 deletions x-pack/plugins/fleet/server/services/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { GLOBAL_SETTINGS_ID, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '../../com

import type { Settings } from '../types';

import { DeleteUnenrolledAgentsPreconfiguredError } from '../errors';

import { appContextService } from './app_context';
import { getSettings, saveSettings, settingsSetup } from './settings';
import { auditLoggingService } from './audit_logging';
Expand Down Expand Up @@ -225,4 +227,119 @@ describe('saveSettings', () => {
});
});
});

it('should allow updating preconfigured setting if called from setup', async () => {
const soClient = savedObjectsClientMock.create();

const newData: Partial<Omit<Settings, 'id'>> = {
delete_unenrolled_agents: {
enabled: true,
is_preconfigured: true,
},
};

soClient.find.mockResolvedValueOnce({
saved_objects: [
{
id: GLOBAL_SETTINGS_ID,
attributes: {
delete_unenrolled_agents: {
enabled: false,
is_preconfigured: true,
},
},
references: [],
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
score: 0,
},
],
page: 1,
per_page: 10,
total: 1,
});
mockListFleetServerHosts.mockResolvedValueOnce({
items: [
{
id: 'fleet-server-host',
name: 'Fleet Server Host',
is_default: true,
is_preconfigured: false,
host_urls: ['http://localhost:8220'],
},
],
page: 1,
perPage: 10,
total: 1,
});

soClient.update.mockResolvedValueOnce({
id: GLOBAL_SETTINGS_ID,
attributes: {},
references: [],
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
});

await saveSettings(soClient, newData, { fromSetup: true });

expect(soClient.update).toHaveBeenCalled();
});

it('should not allow updating preconfigured setting if not called from setup', async () => {
const soClient = savedObjectsClientMock.create();

const newData: Partial<Omit<Settings, 'id'>> = {
delete_unenrolled_agents: {
enabled: true,
is_preconfigured: true,
},
};

soClient.find.mockResolvedValueOnce({
saved_objects: [
{
id: GLOBAL_SETTINGS_ID,
attributes: {
delete_unenrolled_agents: {
enabled: false,
is_preconfigured: true,
},
},
references: [],
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
score: 0,
},
],
page: 1,
per_page: 10,
total: 1,
});
mockListFleetServerHosts.mockResolvedValueOnce({
items: [
{
id: 'fleet-server-host',
name: 'Fleet Server Host',
is_default: true,
is_preconfigured: false,
host_urls: ['http://localhost:8220'],
},
],
page: 1,
perPage: 10,
total: 1,
});

soClient.update.mockResolvedValueOnce({
id: GLOBAL_SETTINGS_ID,
attributes: {},
references: [],
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
});

try {
await saveSettings(soClient, newData);
fail('Expected to throw');
} catch (e) {
expect(e).toBeInstanceOf(DeleteUnenrolledAgentsPreconfiguredError);
}
});
});
Loading

0 comments on commit 147c2d4

Please sign in to comment.