From fa2c399cfc2e2d4a8c93e6cb490b67da2f4c1197 Mon Sep 17 00:00:00 2001 From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:12:08 -0600 Subject: [PATCH] Add label scope to create FMA endpoint (#24830) --- ee/server/service/maintained_apps.go | 14 ++-- server/datastore/mysql/software_installers.go | 2 +- server/service/integration_enterprise_test.go | 77 +++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/ee/server/service/maintained_apps.go b/ee/server/service/maintained_apps.go index 2c83ed39b3..4056ec065a 100644 --- a/ee/server/service/maintained_apps.go +++ b/ee/server/service/maintained_apps.go @@ -32,15 +32,17 @@ func (svc *Service) AddFleetMaintainedApp( return 0, err } - if len(labelsIncludeAny) > 0 && len(labelsExcludeAny) > 0 { - return 0, &fleet.BadRequestError{Message: `Only one of "labels_include_any" or "labels_exclude_any" can be included.`} - } - vc, ok := viewer.FromContext(ctx) if !ok { return 0, fleet.ErrNoContext } + // validate labels before we do anything else + validatedLabels, err := svc.validateSoftwareLabels(ctx, labelsIncludeAny, labelsExcludeAny) + if err != nil { + return 0, ctxerr.Wrap(ctx, err, "validating software labels") + } + app, err := svc.ds.GetMaintainedAppByID(ctx, appID) if err != nil { return 0, ctxerr.Wrap(ctx, err, "getting maintained app by id") @@ -120,11 +122,9 @@ func (svc *Service) AddFleetMaintainedApp( SelfService: selfService, InstallScript: installScript, UninstallScript: uninstallScript, + ValidatedLabels: validatedLabels, } - // TODO: labels validations, for now just use empty struct - payload.ValidatedLabels = &fleet.LabelIdentsWithScope{} - // Create record in software installers table _, titleID, err = svc.ds.MatchOrCreateSoftwareInstaller(ctx, payload) if err != nil { diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index b604f7f84f..5ba939a025 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -274,7 +274,7 @@ func setOrUpdateSoftwareInstallerLabelsDB(ctx context.Context, tx sqlx.ExtContex return ctxerr.New(ctx, "invalid label scope") } - stmt := `INSERT INTO software_installer_labels (software_installer_id, label_id, exclude) VALUES %s ON DUPLICATE KEY UPDATE software_installer_id = software_installer_id, label_id = label_id, exclude = VALUES(exclude)` + stmt := `INSERT INTO software_installer_labels (software_installer_id, label_id, exclude) VALUES %s ON DUPLICATE KEY UPDATE exclude = VALUES(exclude)` var placeholders string var insertArgs []interface{} for _, lid := range labelIds { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index a21ea904b0..2aa4fe75f4 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -15680,6 +15680,83 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { require.NotNil(t, policies[0].InstallSoftware) require.Equal(t, tpResp.Policy.InstallSoftware.Name, policies[0].InstallSoftware.Name) require.Equal(t, tpResp.Policy.InstallSoftware.SoftwareTitleID, policies[0].InstallSoftware.SoftwareTitleID) + + // =========================================================================================== + // Adding label-scoped FMA + // =========================================================================================== + + // Add some labels + var newLabelResp createLabelResponse + s.DoJSON("POST", "/api/v1/fleet/labels", fleet.LabelPayload{ + Name: t.Name() + "1", + Platform: "darwin", + Query: "SELECT 1", + }, http.StatusOK, &newLabelResp) + lbl1 := newLabelResp.Label + s.DoJSON("POST", "/api/v1/fleet/labels", fleet.LabelPayload{ + Name: t.Name() + "2", + Platform: "darwin", + Query: "SELECT 1", + }, http.StatusOK, &newLabelResp) + lbl2 := newLabelResp.Label + + // Add another FMA + req = &addFleetMaintainedAppRequest{ + AppID: 6, + SelfService: false, + PreInstallQuery: "SELECT 1", + InstallScript: "echo foo", + PostInstallScript: "echo done", + TeamID: ptr.Uint(0), + LabelsIncludeAny: []string{lbl1.Name, lbl2.Name}, + } + + addMAResp = addFleetMaintainedAppResponse{} + s.DoJSON("POST", "/api/latest/fleet/software/fleet_maintained_apps", req, http.StatusOK, &addMAResp) + require.NoError(t, addMAResp.Err) + require.NotEmpty(t, addMAResp.SoftwareTitleID) + + // Get software title details + titleResp = getSoftwareTitleResponse{} + s.DoJSON( + "GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", addMAResp.SoftwareTitleID), + getSoftwareTitleRequest{}, + http.StatusOK, &titleResp, + "team_id", "0", + ) + + require.NotNil(t, titleResp.SoftwareTitle) + swTitle = titleResp.SoftwareTitle + require.NotNil(t, swTitle.SoftwarePackage) + require.Empty(t, swTitle.SoftwarePackage.LabelsExcludeAny) + require.Len(t, swTitle.SoftwarePackage.LabelsIncludeAny, 2) + gotNames := make(map[string]bool) + for _, lbl := range swTitle.SoftwarePackage.LabelsIncludeAny { + gotNames[lbl.LabelName] = true + } + require.True(t, gotNames[lbl1.Name]) + require.True(t, gotNames[lbl2.Name]) + + // Can't set non-existent label + req = &addFleetMaintainedAppRequest{ + AppID: 7, + SelfService: false, + PreInstallQuery: "SELECT 1", + InstallScript: "echo foo", + PostInstallScript: "echo done", + TeamID: ptr.Uint(0), + LabelsIncludeAny: []string{"no-such-label"}, + } + addMAResp = addFleetMaintainedAppResponse{} + r = s.Do("POST", "/api/latest/fleet/software/fleet_maintained_apps", req, http.StatusBadRequest) + require.Contains(t, extractServerErrorText(r.Body), "some or all the labels provided don't exist") + + // Can't set both labels_include_any and labels_exclude_any + req.LabelsIncludeAny = []string{lbl1.Name, lbl2.Name} + req.LabelsExcludeAny = []string{lbl1.Name} + addMAResp = addFleetMaintainedAppResponse{} + r = s.Do("POST", "/api/latest/fleet/software/fleet_maintained_apps", req, http.StatusBadRequest) + require.Contains(t, extractServerErrorText(r.Body), `Only one of "labels_include_any" or "labels_exclude_any" can be included`) } func (s *integrationEnterpriseTestSuite) TestWindowsMigrateMDMNotEnabled() {