From 2a8e93311ba3aa5d6bf19145c9fb56edaca7b18f Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Thu, 27 Oct 2022 09:49:40 +0100 Subject: [PATCH] [Fleet] Make asset tags space aware (#144066) * fix: use space ID in managed tag SO ID * Add SO migration * add integration test for installing pkg in 2 spaces * Revert "Add SO migration" This reverts commit 4aeeea658c79d30cfb7ad96090b87418a3b72ad2. * use legacy tags if they exist * add tags integration test * test working in isolation * neaten tests * remove test pkg * revert test file * tidy for PR * fix type errors --- .../services/epm/kibana/assets/install.ts | 4 +- .../epm/kibana/assets/tag_assets.test.ts | 152 +++++++++++++++-- .../services/epm/kibana/assets/tag_assets.ts | 127 ++++++++++----- .../services/epm/packages/_install_package.ts | 1 + .../fleet_api_integration/apis/epm/index.js | 1 + .../apis/epm/install_tag_assets.ts | 154 ++++++++++++++++++ 6 files changed, 380 insertions(+), 59 deletions(-) create mode 100644 x-pack/test/fleet_api_integration/apis/epm/install_tag_assets.ts diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index 92ea60d290040..2cf665e0fc094 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -141,6 +141,7 @@ export async function installKibanaAssetsAndReferences({ pkgTitle, paths, installedPkg, + spaceId, }: { savedObjectsClient: SavedObjectsClientContract; savedObjectsImporter: Pick; @@ -151,6 +152,7 @@ export async function installKibanaAssetsAndReferences({ pkgTitle: string; paths: string[]; installedPkg?: SavedObject; + spaceId: string; }) { const kibanaAssets = await getKibanaAssets(paths); if (installedPkg) await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg }); @@ -167,7 +169,6 @@ export async function installKibanaAssetsAndReferences({ pkgName, kibanaAssets, }); - await withPackageSpan('Create and assign package tags', () => tagKibanaAssets({ savedObjectTagAssignmentService, @@ -175,6 +176,7 @@ export async function installKibanaAssetsAndReferences({ kibanaAssets, pkgTitle, pkgName, + spaceId, }) ); diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.test.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.test.ts index 3c946217d36b4..d887631240175 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.test.ts @@ -11,18 +11,18 @@ describe('tagKibanaAssets', () => { updateTagAssignments: jest.fn(), } as any; const savedObjectTagClient = { - getAll: jest.fn(), + get: jest.fn(), create: jest.fn(), } as any; beforeEach(() => { savedObjectTagAssignmentService.updateTagAssignments.mockReset(); - savedObjectTagClient.getAll.mockReset(); + savedObjectTagClient.get.mockReset(); savedObjectTagClient.create.mockReset(); }); - it('should create Managed and System tags when tagKibanaAssets with System package', async () => { - savedObjectTagClient.getAll.mockResolvedValue([]); + it('should create Managed and System tags when tagKibanaAssets with System package when no tags exist', async () => { + savedObjectTagClient.get.mockRejectedValue(new Error('not found')); savedObjectTagClient.create.mockImplementation(({ name }: { name: string }) => Promise.resolve({ id: name.toLowerCase(), name }) ); @@ -34,6 +34,7 @@ describe('tagKibanaAssets', () => { kibanaAssets, pkgTitle: 'System', pkgName: 'system', + spaceId: 'default', }); expect(savedObjectTagClient.create).toHaveBeenCalledWith( @@ -42,7 +43,7 @@ describe('tagKibanaAssets', () => { description: '', color: '#FFFFFF', }, - { id: 'managed', overwrite: true, refresh: false } + { id: 'fleet-managed-default', overwrite: true, refresh: false } ); expect(savedObjectTagClient.create).toHaveBeenCalledWith( { @@ -50,10 +51,10 @@ describe('tagKibanaAssets', () => { description: '', color: '#FFFFFF', }, - { id: 'system', overwrite: true, refresh: false } + { id: 'fleet-pkg-system-default', overwrite: true, refresh: false } ); expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({ - tags: ['managed', 'system'], + tags: ['fleet-managed-default', 'fleet-pkg-system-default'], assign: kibanaAssets.dashboard, unassign: [], refresh: false, @@ -61,10 +62,7 @@ describe('tagKibanaAssets', () => { }); it('should only assign Managed and System tags when tags already exist', async () => { - savedObjectTagClient.getAll.mockResolvedValue([ - { id: 'managed', name: 'Managed' }, - { id: 'system', name: 'System' }, - ]); + savedObjectTagClient.get.mockResolvedValue({ name: '', color: '', description: '' }); const kibanaAssets = { dashboard: [{ id: 'dashboard1', type: 'dashboard' }] } as any; await tagKibanaAssets({ @@ -73,11 +71,12 @@ describe('tagKibanaAssets', () => { kibanaAssets, pkgTitle: 'System', pkgName: 'system', + spaceId: 'default', }); expect(savedObjectTagClient.create).not.toHaveBeenCalled(); expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({ - tags: ['managed', 'system'], + tags: ['fleet-managed-default', 'fleet-pkg-system-default'], assign: kibanaAssets.dashboard, unassign: [], refresh: false, @@ -85,7 +84,7 @@ describe('tagKibanaAssets', () => { }); it('should skip non taggable asset types', async () => { - savedObjectTagClient.getAll.mockResolvedValue([]); + savedObjectTagClient.get.mockRejectedValue(new Error('tag not found')); savedObjectTagClient.create.mockImplementation(({ name }: { name: string }) => Promise.resolve({ id: name.toLowerCase(), name }) ); @@ -104,10 +103,11 @@ describe('tagKibanaAssets', () => { kibanaAssets, pkgTitle: 'System', pkgName: 'system', + spaceId: 'default', }); expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({ - tags: ['managed', 'system'], + tags: ['fleet-managed-default', 'fleet-pkg-system-default'], assign: [ ...kibanaAssets.dashboard, ...kibanaAssets.search, @@ -129,8 +129,132 @@ describe('tagKibanaAssets', () => { kibanaAssets, pkgTitle: 'System', pkgName: 'system', + spaceId: 'default', }); expect(savedObjectTagAssignmentService.updateTagAssignments).not.toHaveBeenCalled(); }); + + it('should use legacy managed tag if it exists', async () => { + savedObjectTagClient.get.mockImplementation(async (id: string) => { + if (id === 'managed') return { name: 'managed', description: '', color: '' }; + + throw new Error('not found'); + }); + + savedObjectTagClient.create.mockImplementation(({ name }: { name: string }) => + Promise.resolve({ id: name.toLowerCase(), name }) + ); + const kibanaAssets = { dashboard: [{ id: 'dashboard1', type: 'dashboard' }] } as any; + + await tagKibanaAssets({ + savedObjectTagAssignmentService, + savedObjectTagClient, + kibanaAssets, + pkgTitle: 'System', + pkgName: 'system', + spaceId: 'default', + }); + + expect(savedObjectTagClient.create).not.toHaveBeenCalledWith( + { + name: 'Managed', + description: '', + color: '#FFFFFF', + }, + { id: 'fleet-managed-default', overwrite: true, refresh: false } + ); + + expect(savedObjectTagClient.create).toHaveBeenCalledWith( + { + name: 'System', + description: '', + color: '#FFFFFF', + }, + { id: 'fleet-pkg-system-default', overwrite: true, refresh: false } + ); + expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({ + tags: ['managed', 'fleet-pkg-system-default'], + assign: kibanaAssets.dashboard, + unassign: [], + refresh: false, + }); + }); + + it('should use legacy package tag if it exists', async () => { + savedObjectTagClient.get.mockImplementation(async (id: string) => { + if (id === 'system') return { name: 'system', description: '', color: '' }; + + throw new Error('not found'); + }); + + savedObjectTagClient.create.mockImplementation(({ name }: { name: string }) => + Promise.resolve({ id: name.toLowerCase(), name }) + ); + const kibanaAssets = { dashboard: [{ id: 'dashboard1', type: 'dashboard' }] } as any; + + await tagKibanaAssets({ + savedObjectTagAssignmentService, + savedObjectTagClient, + kibanaAssets, + pkgTitle: 'System', + pkgName: 'system', + spaceId: 'default', + }); + + expect(savedObjectTagClient.create).toHaveBeenCalledWith( + { + name: 'Managed', + description: '', + color: '#FFFFFF', + }, + { id: 'fleet-managed-default', overwrite: true, refresh: false } + ); + + expect(savedObjectTagClient.create).not.toHaveBeenCalledWith( + { + name: 'System', + description: '', + color: '#FFFFFF', + }, + { id: 'system', overwrite: true, refresh: false } + ); + expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({ + tags: ['fleet-managed-default', 'system'], + assign: kibanaAssets.dashboard, + unassign: [], + refresh: false, + }); + }); + + it('should use both legacy tags if they exist', async () => { + savedObjectTagClient.get.mockImplementation(async (id: string) => { + if (id === 'managed') return { name: 'managed', description: '', color: '' }; + if (id === 'system') return { name: 'system', description: '', color: '' }; + + throw new Error('not found'); + }); + + savedObjectTagClient.create.mockImplementation(({ name }: { name: string }) => + Promise.resolve({ id: name.toLowerCase(), name }) + ); + const kibanaAssets = { dashboard: [{ id: 'dashboard1', type: 'dashboard' }] } as any; + + await tagKibanaAssets({ + savedObjectTagAssignmentService, + savedObjectTagClient, + kibanaAssets, + pkgTitle: 'System', + pkgName: 'system', + spaceId: 'default', + }); + + expect(savedObjectTagClient.create).not.toHaveBeenCalled(); + expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({ + tags: ['managed', 'system'], + assign: kibanaAssets.dashboard, + unassign: [], + refresh: false, + }); + }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.ts index 842932d71359e..1d61c3c908872 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.ts @@ -15,22 +15,45 @@ import { KibanaSavedObjectTypeMapping } from './install'; const TAG_COLOR = '#FFFFFF'; const MANAGED_TAG_NAME = 'Managed'; -const MANAGED_TAG_ID = 'managed'; - -export async function tagKibanaAssets({ - savedObjectTagAssignmentService, - savedObjectTagClient, - kibanaAssets, - pkgTitle, - pkgName, -}: { +const LEGACY_MANAGED_TAG_ID = 'managed'; + +const getManagedTagId = (spaceId: string) => `fleet-managed-${spaceId}`; +const getPackageTagId = (spaceId: string, pkgName: string) => `fleet-pkg-${pkgName}-${spaceId}`; +const getLegacyPackageTagId = (pkgName: string) => pkgName; + +interface TagAssetsParams { savedObjectTagAssignmentService: IAssignmentService; savedObjectTagClient: ITagsClient; kibanaAssets: Record; pkgTitle: string; pkgName: string; -}) { - const taggableAssets = Object.entries(kibanaAssets).flatMap(([assetType, assets]) => { + spaceId: string; +} + +export async function tagKibanaAssets(opts: TagAssetsParams) { + const { savedObjectTagAssignmentService, kibanaAssets } = opts; + const taggableAssets = getTaggableAssets(kibanaAssets); + + // no assets to tag + if (taggableAssets.length === 0) { + return; + } + + const [managedTagId, packageTagId] = await Promise.all([ + ensureManagedTag(opts), + ensurePackageTag(opts), + ]); + + await savedObjectTagAssignmentService.updateTagAssignments({ + tags: [managedTagId, packageTagId], + assign: taggableAssets, + unassign: [], + refresh: false, + }); +} + +function getTaggableAssets(kibanaAssets: TagAssetsParams['kibanaAssets']) { + return Object.entries(kibanaAssets).flatMap(([assetType, assets]) => { if (!taggableTypes.includes(KibanaSavedObjectTypeMapping[assetType as KibanaAssetType])) { return []; } @@ -41,41 +64,57 @@ export async function tagKibanaAssets({ return assets; }); +} - // no assets to tag - if (taggableAssets.length === 0) { - return; - } +async function ensureManagedTag( + opts: Pick +): Promise { + const { spaceId, savedObjectTagClient } = opts; - const allTags = await savedObjectTagClient.getAll(); - let managedTag = allTags.find((tag) => tag.name === MANAGED_TAG_NAME); - if (!managedTag) { - managedTag = await savedObjectTagClient.create( - { - name: MANAGED_TAG_NAME, - description: '', - color: TAG_COLOR, - }, - { id: MANAGED_TAG_ID, overwrite: true, refresh: false } - ); - } + const managedTagId = getManagedTagId(spaceId); + const managedTag = await savedObjectTagClient.get(managedTagId).catch(() => {}); - let packageTag = allTags.find((tag) => tag.name === pkgTitle); - if (!packageTag) { - packageTag = await savedObjectTagClient.create( - { - name: pkgTitle, - description: '', - color: TAG_COLOR, - }, - { id: pkgName, overwrite: true, refresh: false } - ); - } + if (managedTag) return managedTagId; - await savedObjectTagAssignmentService.updateTagAssignments({ - tags: [managedTag.id, packageTag.id], - assign: taggableAssets, - unassign: [], - refresh: false, - }); + const legacyManagedTag = await savedObjectTagClient.get(LEGACY_MANAGED_TAG_ID).catch(() => {}); + + if (legacyManagedTag) return LEGACY_MANAGED_TAG_ID; + + await savedObjectTagClient.create( + { + name: MANAGED_TAG_NAME, + description: '', + color: TAG_COLOR, + }, + { id: managedTagId, overwrite: true, refresh: false } + ); + + return managedTagId; +} + +async function ensurePackageTag( + opts: Pick +): Promise { + const { spaceId, savedObjectTagClient, pkgName, pkgTitle } = opts; + + const packageTagId = getPackageTagId(spaceId, pkgName); + const packageTag = await savedObjectTagClient.get(packageTagId).catch(() => {}); + + if (packageTag) return packageTagId; + + const legacyPackageTagId = getLegacyPackageTagId(pkgName); + const legacyPackageTag = await savedObjectTagClient.get(legacyPackageTagId).catch(() => {}); + + if (legacyPackageTag) return legacyPackageTagId; + + await savedObjectTagClient.create( + { + name: pkgTitle, + description: '', + color: TAG_COLOR, + }, + { id: packageTagId, overwrite: true, refresh: false } + ); + + return packageTagId; } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 4ecec17560731..78683ecd07e0a 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -133,6 +133,7 @@ export async function _installPackage({ paths, installedPkg, logger, + spaceId, }) ); // Necessary to avoid async promise rejection warning diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index 137d7d59d8bfa..48af135f15ae2 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -23,6 +23,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./install_remove_kbn_assets_in_space')); loadTestFile(require.resolve('./install_remove_multiple')); loadTestFile(require.resolve('./install_update')); + loadTestFile(require.resolve('./install_tag_assets')); loadTestFile(require.resolve('./bulk_upgrade')); loadTestFile(require.resolve('./update_assets')); loadTestFile(require.resolve('./data_stream')); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_tag_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_tag_assets.ts new file mode 100644 index 0000000000000..7458912207a38 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/epm/install_tag_assets.ts @@ -0,0 +1,154 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { setupFleetAndAgents } from '../agents/services'; +const testSpaceId = 'fleet_test_space'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); + const pkgName = 'only_dashboard'; + const pkgVersion = '0.1.0'; + + const uninstallPackage = async (pkg: string, version: string) => { + await supertest.delete(`/api/fleet/epm/packages/${pkg}/${version}`).set('kbn-xsrf', 'xxxx'); + }; + + const installPackageInSpace = async (pkg: string, version: string, spaceId: string) => { + await supertest + .post(`/s/${spaceId}/api/fleet/epm/packages/${pkg}/${version}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }; + const createSpace = async (spaceId: string) => { + await supertest + .post(`/api/spaces/space`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: spaceId, + id: spaceId, + initials: 's', + color: '#D6BF57', + disabledFeatures: [], + imageUrl: '', + }) + .expect(200); + }; + + const getTag = async (id: string, space?: string) => + kibanaServer.savedObjects + .get({ + type: 'tag', + id, + ...(space && { space }), + }) + .catch(() => {}); + + const deleteTag = async (id: string) => + kibanaServer.savedObjects + .delete({ + type: 'tag', + id, + }) + .catch(() => {}); + + const deleteSpace = async (spaceId: string) => { + await supertest.delete(`/api/spaces/space/${spaceId}`).set('kbn-xsrf', 'xxxx').send(); + }; + describe('asset tagging', () => { + skipIfNoDockerRegistry(providerContext); + setupFleetAndAgents(providerContext); + before(async () => { + await createSpace(testSpaceId); + }); + + after(async () => { + await deleteSpace(testSpaceId); + }); + describe('creates correct tags when installing a package in non default space after installing in default space', async () => { + before(async () => { + if (!server.enabled) return; + await installPackageInSpace('all_assets', pkgVersion, 'default'); + await installPackageInSpace(pkgName, pkgVersion, testSpaceId); + }); + after(async () => { + if (!server.enabled) return; + await uninstallPackage('all_assets', pkgVersion); + await uninstallPackage(pkgName, pkgVersion); + }); + + it('Should create managed tag saved objects', async () => { + const defaultTag = await getTag('fleet-managed-default'); + expect(defaultTag).not.equal(undefined); + + const spaceTag = await getTag('fleet-managed-fleet_test_space', testSpaceId); + expect(spaceTag).not.equal(undefined); + }); + it('Should create package tag saved objects', async () => { + const defaultTag = await getTag(`fleet-pkg-all_assets-default`); + expect(defaultTag).not.equal(undefined); + + const spaceTag = await getTag(`fleet-pkg-${pkgName}-fleet_test_space`, testSpaceId); + expect(spaceTag).not.equal(undefined); + }); + }); + + describe('Handles presence of legacy tags', async () => { + before(async () => { + if (!server.enabled) return; + + // first clean up any existing tag saved objects as they arent cleaned on uninstall + await deleteTag('fleet-managed-default'); + await deleteTag(`fleet-pkg-${pkgName}-default`); + + // now create the legacy tags + await kibanaServer.savedObjects.create({ + type: 'tag', + id: 'managed', + overwrite: false, + attributes: { + name: 'managed', + description: '', + color: '#FFFFFF', + }, + }); + await kibanaServer.savedObjects.create({ + type: 'tag', + id: pkgName, + overwrite: false, + attributes: { + name: pkgName, + description: '', + color: '#FFFFFF', + }, + }); + + await installPackageInSpace(pkgName, pkgVersion, 'default'); + }); + after(async () => { + if (!server.enabled) return; + await uninstallPackage(pkgName, pkgVersion); + await deleteTag('managed'); + await deleteTag('tag'); + }); + + it('Should not create space aware tag saved objects if legacy tags exist', async () => { + const managedTag = await getTag('fleet-managed-default'); + expect(managedTag).equal(undefined); + + const pkgTag = await getTag(`fleet-pkg-${pkgName}-default`); + expect(pkgTag).equal(undefined); + }); + }); + }); +}