diff --git a/docs/Contributing/Audit-logs.md b/docs/Contributing/Audit-logs.md index ed9bdcd7122a..1b4b6830c7c9 100644 --- a/docs/Contributing/Audit-logs.md +++ b/docs/Contributing/Audit-logs.md @@ -1474,6 +1474,45 @@ This activity contains the following fields: } ``` +## updated_app_store_app + +Generated when an App Store app is updated in Fleet. + +This activity contains the following fields: +- "software_title": Name of the App Store app. +- "software_title_id": ID of the updated app's software title. +- "app_store_id": ID of the app on the Apple App Store. +- "platform": Platform of the app (`darwin`, `ios`, or `ipados`). +- "self_service": App installation can be initiated by device owner. +- "team_name": Name of the team on which this App Store app was updated, or `null` if it was updated on no team. +- "team_id": ID of the team on which this App Store app was updated, or `null`if it was updated on no team. +- "labels_include_any": Target hosts that have any label in the array. +- "labels_exclude_any": Target hosts that don't have any label in the array. + +#### Example + +```json +{ + "software_title": "Logic Pro", + "software_title_id": 123, + "app_store_id": "1234567", + "platform": "darwin", + "self_service": true, + "team_name": "Workstations", + "team_id": 1, + "labels_include_any": [ + { + "name": "Engineering", + "id": 12 + }, + { + "name": "Product", + "id": 17 + } + ] +} +``` + ## added_ndes_scep_proxy Generated when NDES SCEP proxy is configured in Fleet. diff --git a/ee/server/service/vpp.go b/ee/server/service/vpp.go index 591d8d213aaf..3326554f1dc9 100644 --- a/ee/server/service/vpp.go +++ b/ee/server/service/vpp.go @@ -451,19 +451,19 @@ func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.V return apps, nil } -func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny []string) error { +func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny []string) (*fleet.VPPAppStoreApp, error) { if err := svc.authz.Authorize(ctx, &fleet.VPPApp{TeamID: teamID}, fleet.ActionWrite); err != nil { - return err + return nil, err } var teamName string if teamID != nil && *teamID != 0 { tm, err := svc.ds.Team(ctx, *teamID) if fleet.IsNotFound(err) { - return fleet.NewInvalidArgumentError("team_id", fmt.Sprintf("team %d does not exist", *teamID)). + return nil, fleet.NewInvalidArgumentError("team_id", fmt.Sprintf("team %d does not exist", *teamID)). WithStatus(http.StatusNotFound) } else if err != nil { - return ctxerr.Wrap(ctx, err, "checking if team exists") + return nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: checking if team exists") } teamName = tm.Name @@ -471,16 +471,16 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID validatedLabels, err := ValidateSoftwareLabels(ctx, svc, labelsIncludeAny, labelsExcludeAny) if err != nil { - return ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: validating software labels") + return nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: validating software labels") } meta, err := svc.ds.GetVPPAppMetadataByTeamAndTitleID(ctx, teamID, titleID) if err != nil { - return ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: getting vpp app metadata") + return nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: getting vpp app metadata") } if selfService && meta.Platform != fleet.MacOSPlatform { - return fleet.NewUserMessageError(errors.New("Currently, self-service only supports macOS"), http.StatusBadRequest) + return nil, fleet.NewUserMessageError(errors.New("Currently, self-service only supports macOS"), http.StatusBadRequest) } appToWrite := &fleet.VPPApp{ @@ -503,7 +503,7 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID _, err = svc.ds.InsertVPPAppWithTeam(ctx, appToWrite, teamID) if err != nil { - return ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: write app to db") + return nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: write app to db") } actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(validatedLabels) @@ -520,10 +520,17 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID LabelsExcludeAny: actLabelsExcl, } if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { - return ctxerr.Wrap(ctx, err, "create activity for update app store app") + return nil, ctxerr.Wrap(ctx, err, "create activity for update app store app") } - return nil + updatedAppMeta, err := svc.ds.GetVPPAppMetadataByTeamAndTitleID(ctx, teamID, titleID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: getting updated app metadata") + } + + updatedAppMeta.Platform = "" + + return updatedAppMeta, nil } func (svc *Service) UploadVPPToken(ctx context.Context, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { diff --git a/package.json b/package.json index 559abc0779de..f8e87872d2a7 100644 --- a/package.json +++ b/package.json @@ -183,5 +183,6 @@ "browserslist": [ "defaults" ], - "license": "SEE LICENSE IN ./LICENSE" + "license": "SEE LICENSE IN ./LICENSE", + "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" } diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 546d3b587379..dbcab51e6f3b 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -111,6 +111,7 @@ var ActivityDetailsList = []ActivityDetails{ ActivityAddedAppStoreApp{}, ActivityDeletedAppStoreApp{}, ActivityInstalledAppStoreApp{}, + ActivityUpdatedAppStoreApp{}, ActivityAddedNDESSCEPProxy{}, ActivityDeletedNDESSCEPProxy{}, @@ -2077,14 +2078,14 @@ func (a ActivityUpdatedAppStoreApp) ActivityName() string { func (a ActivityUpdatedAppStoreApp) Documentation() (activity string, details string, detailsExample string) { return "Generated when an App Store app is updated in Fleet.", `This activity contains the following fields: - "software_title": Name of the App Store app. -- "software_title_id": ID of the updated software title. +- "software_title_id": ID of the updated app's software title. - "app_store_id": ID of the app on the Apple App Store. - "platform": Platform of the app (` + "`darwin`, `ios`, or `ipados`" + `). - "self_service": App installation can be initiated by device owner. - "team_name": Name of the team on which this App Store app was updated, or ` + "`null`" + ` if it was updated on no team. - "team_id": ID of the team on which this App Store app was updated, or ` + "`null`" + `if it was updated on no team. - "labels_include_any": Target hosts that have any label in the array. -- "labels_exclude_any": Target hosts that don't have any label in the array`, `{ +- "labels_exclude_any": Target hosts that don't have any label in the array.`, `{ "software_title": "Logic Pro", "software_title_id": 123, "app_store_id": "1234567", diff --git a/server/fleet/service.go b/server/fleet/service.go index 5200c2a68f49..7c73b138dd1b 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -685,7 +685,7 @@ type Service interface { GetAppStoreApps(ctx context.Context, teamID *uint) ([]*VPPApp, error) AddAppStoreApp(ctx context.Context, teamID *uint, appTeam VPPAppTeam) error - UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny []string) error + UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny []string) (*VPPAppStoreApp, error) // MDMAppleProcessOTAEnrollment handles OTA enrollment requests. // diff --git a/server/fleet/vpp.go b/server/fleet/vpp.go index 6339c891c711..1f7a34db50fc 100644 --- a/server/fleet/vpp.go +++ b/server/fleet/vpp.go @@ -47,9 +47,9 @@ type VPPApp struct { TeamID *uint `db:"-" json:"team_id,omitempty"` TitleID uint `db:"title_id" json:"-"` - CreatedAt time.Time `db:"created_at" json:"-"` - UpdatedAt time.Time `db:"updated_at" json:"-"` - ValidatedLabels *LabelIdentsWithScope + CreatedAt time.Time `db:"created_at" json:"-"` + UpdatedAt time.Time `db:"updated_at" json:"-"` + ValidatedLabels *LabelIdentsWithScope `json:"-"` } // AuthzType implements authz.AuthzTyper. diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index ba175df27065..de0579626c7d 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -11095,7 +11095,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() { require.Len(t, resp.SoftwareTitles, 1) nonVPPTitleID := resp.SoftwareTitles[0].ID - updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, LabelsIncludeAny: []string{l2.Name}} + updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false} // Attempt to update the non-VPP software using the VPP path. Should fail. s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", nonVPPTitleID), updateAppReq, http.StatusNotFound) @@ -11103,15 +11103,35 @@ func (s *integrationMDMTestSuite) TestVPPApps() { // Attempt tp update a non-existent app. Should fail. s.Do("PATCH", "/api/latest/fleet/software/titles/9999/app_store_app", updateAppReq, http.StatusNotFound) - // Update App2. Unset self service, - s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusOK) + // Attempt to update with both types of labels. Should fail. + updateAppReq.LabelsIncludeAny = []string{l1.Name} + updateAppReq.LabelsExcludeAny = []string{l1.Name} + res = s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusBadRequest) + require.Contains(t, extractServerErrorText(res.Body), `Only one of "labels_include_any" or "labels_exclude_any" can be included.`) + + // Attempt to update with a non-existent label. Should fail. + updateAppReq.LabelsExcludeAny = []string{} + updateAppReq.LabelsIncludeAny = []string{"404_notfound"} + res = s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusBadRequest) + require.Contains(t, extractServerErrorText(res.Body), "some or all the labels provided don't exist") + + // Update App2. Unset self service and update the labels + updateAppReq.LabelsIncludeAny = []string{l2.Name} + var updateAppResp updateAppStoreAppResponse + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusOK, &updateAppResp) + + require.NotNil(t, updateAppResp.AppStoreApp) + require.Equal(t, updateAppResp.AppStoreApp.AdamID, excludeAnyApp.AdamID) + require.Equal(t, updateAppResp.AppStoreApp.LabelsIncludeAny, []fleet.SoftwareScopeLabel{{LabelName: l2.Name, LabelID: l2.ID}}) + require.Empty(t, updateAppResp.AppStoreApp.LabelsExcludeAny) + require.False(t, updateAppResp.AppStoreApp.SelfService) activityData = `{"team_name": "%s", "software_title": "%s", "app_store_id": "%s", "team_id": %d, "software_title_id": %d, "platform": "%s", "self_service": false, "labels_include_any": [{"id": %d, "name": %q}]}` s.lastActivityMatches(fleet.ActivityUpdatedAppStoreApp{}.ActivityName(), fmt.Sprintf(activityData, team.Name, excludeAnyApp.Name, excludeAnyApp.AdamID, team.ID, titleID, excludeAnyApp.Platform, l2.ID, l2.Name), 0) - // check that our updates worked + // double check that our updates worked getSWTitle = getSoftwareTitleResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &getSWTitle, "team_id", fmt.Sprint(team.ID)) require.NotNil(t, getSWTitle.SoftwareTitle.AppStoreApp) diff --git a/server/service/vpp.go b/server/service/vpp.go index 1100633c039e..4e62367ad7c3 100644 --- a/server/service/vpp.go +++ b/server/service/vpp.go @@ -99,7 +99,8 @@ type updateAppStoreAppRequest struct { } type updateAppStoreAppResponse struct { - Err error `json:"error,omitempty"` + AppStoreApp *fleet.VPPAppStoreApp `json:"app_store_app,omitempty"` + Err error `json:"error,omitempty"` } func (r updateAppStoreAppResponse) error() error { return r.Err } @@ -107,19 +108,20 @@ func (r updateAppStoreAppResponse) error() error { return r.Err } func updateAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*updateAppStoreAppRequest) - if err := svc.UpdateAppStoreApp(ctx, req.TitleID, req.TeamID, req.SelfService, req.LabelsIncludeAny, req.LabelsExcludeAny); err != nil { + updatedApp, err := svc.UpdateAppStoreApp(ctx, req.TitleID, req.TeamID, req.SelfService, req.LabelsIncludeAny, req.LabelsExcludeAny) + if err != nil { return updateAppStoreAppResponse{Err: err}, nil } - return updateAppStoreAppResponse{}, nil + return updateAppStoreAppResponse{AppStoreApp: updatedApp}, nil } -func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny []string) error { +func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny []string) (*fleet.VPPAppStoreApp, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) - return fleet.ErrMissingLicense + return nil, fleet.ErrMissingLicense } ////////////////////////////////////////////////////////////////////////////////