diff --git a/changes/21777-list-fleet-manated-apps b/changes/21777-list-fleet-manated-apps new file mode 100644 index 000000000000..7da57b618796 --- /dev/null +++ b/changes/21777-list-fleet-manated-apps @@ -0,0 +1 @@ +- Add API endpoint to list team available Fleet-maintained apps diff --git a/ee/server/service/software.go b/ee/server/service/software.go index 3b66ae485a30..4bc973af1ee2 100644 --- a/ee/server/service/software.go +++ b/ee/server/service/software.go @@ -3,6 +3,7 @@ package service import ( "context" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" ) @@ -16,3 +17,18 @@ func (svc *Service) SoftwareByID(ctx context.Context, id uint, teamID *uint, _ b // reuse SoftwareByID, but include cve scores in premium version return svc.Service.SoftwareByID(ctx, id, teamID, true) } + +func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) { + if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{ + TeamID: &teamID, + }, fleet.ActionRead); err != nil { + return nil, nil, err + } + + avail, meta, err := svc.ds.ListAvailableFleetMaintainedApps(ctx, teamID, opts) + if err != nil { + return nil, nil, ctxerr.Wrap(ctx, err, "listing available fleet managed apps") + } + + return avail, meta, nil +} diff --git a/server/datastore/mysql/maintained_apps.go b/server/datastore/mysql/maintained_apps.go index 45f97ee5a21e..cb849bf216fd 100644 --- a/server/datastore/mysql/maintained_apps.go +++ b/server/datastore/mysql/maintained_apps.go @@ -95,3 +95,51 @@ WHERE return &app, nil } + +func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) { + stmt := ` +SELECT + fla.id, + fla.name, + fla.version, + fla.platform +FROM + fleet_library_apps fla +WHERE NOT EXISTS ( + SELECT + 1 + FROM + software_titles st + LEFT JOIN + software_installers si + ON si.title_id = st.id + LEFT JOIN + vpp_apps va + ON va.title_id = st.id + LEFT JOIN + vpp_apps_teams vat + ON vat.adam_id = va.adam_id + WHERE + st.bundle_identifier = fla.bundle_identifier + AND ( + (si.platform = fla.platform AND si.team_id = ?) + OR + (va.platform = fla.platform AND vat.team_id = ?) + ) +)` + + stmtPaged, args := appendListOptionsWithCursorToSQL(stmt, []any{teamID, teamID}, &opt) + + var avail []fleet.MaintainedApp + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmtPaged, args...); err != nil { + return nil, nil, ctxerr.Wrap(ctx, err, "selecting available fleet managed apps") + } + + meta := &fleet.PaginationMetadata{HasPreviousResults: opt.Page > 0} + if len(avail) > int(opt.PerPage) { + meta.HasNextResults = true + avail = avail[:len(avail)-1] + } + + return avail, meta, nil +} diff --git a/server/datastore/mysql/maintained_apps_test.go b/server/datastore/mysql/maintained_apps_test.go index 4e99c1d2c357..9eb6a6fa0f53 100644 --- a/server/datastore/mysql/maintained_apps_test.go +++ b/server/datastore/mysql/maintained_apps_test.go @@ -7,6 +7,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps" + "github.com/fleetdm/fleet/v4/server/test" "github.com/go-kit/kit/log" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" @@ -21,6 +22,7 @@ func TestMaintainedApps(t *testing.T) { }{ {"UpsertMaintainedApps", testUpsertMaintainedApps}, {"IngestWithBrew", testIngestWithBrew}, + {"ListAvailableApps", testListAvailableApps}, {"GetMaintainedAppByID", testGetMaintainedAppByID}, } @@ -35,8 +37,8 @@ func TestMaintainedApps(t *testing.T) { func testUpsertMaintainedApps(t *testing.T, ds *Datastore) { ctx := context.Background() - listSavedApps := func() []*fleet.MaintainedApp { - var apps []*fleet.MaintainedApp + listSavedApps := func() []fleet.MaintainedApp { + var apps []fleet.MaintainedApp ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.SelectContext(ctx, q, &apps, "SELECT name, version, platform FROM fleet_library_apps ORDER BY token") }) @@ -61,9 +63,9 @@ func testUpsertMaintainedApps(t *testing.T, ds *Datastore) { require.NoError(t, err) // change the expected app data for figma - for _, app := range expectedApps { - if app.Name == "Figma" { - app.Version = "999.9.9" + for idx := range expectedApps { + if expectedApps[idx].Name == "Figma" { + expectedApps[idx].Version = "999.9.9" break } } @@ -87,6 +89,238 @@ func testIngestWithBrew(t *testing.T, ds *Datastore) { require.ElementsMatch(t, expectedTokens, actualTokens) } +func testListAvailableApps(t *testing.T, ds *Datastore) { + ctx := context.Background() + + user := test.NewUser(t, ds, "Zaphod Beeblebrox", "zaphod@example.com", true) + team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 1"}) + require.NoError(t, err) + team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 2"}) + require.NoError(t, err) + + maintained1, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ + Name: "Maintained1", + Token: "maintained1", + Version: "1.0.0", + Platform: fleet.MacOSPlatform, + InstallerURL: "http://example.com/main1", + SHA256: "DEADBEEF", + BundleIdentifier: "fleet.maintained1", + InstallScript: "echo installed", + UninstallScript: "echo uninstalled", + }) + require.NoError(t, err) + maintained2, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ + Name: "Maintained2", + Token: "maintained2", + Version: "1.0.0", + Platform: fleet.MacOSPlatform, + InstallerURL: "http://example.com/main1", + SHA256: "DEADBEEF", + BundleIdentifier: "fleet.maintained2", + InstallScript: "echo installed", + UninstallScript: "echo uninstalled", + }) + require.NoError(t, err) + maintained3, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ + Name: "Maintained3", + Token: "maintained3", + Version: "1.0.0", + Platform: fleet.MacOSPlatform, + InstallerURL: "http://example.com/main1", + SHA256: "DEADBEEF", + BundleIdentifier: "fleet.maintained3", + InstallScript: "echo installed", + UninstallScript: "echo uninstalled", + }) + require.NoError(t, err) + + expectedApps := []fleet.MaintainedApp{ + { + ID: maintained1.ID, + Name: maintained1.Name, + Version: maintained1.Version, + Platform: maintained1.Platform, + }, + { + ID: maintained2.ID, + Name: maintained2.Name, + Version: maintained2.Version, + Platform: maintained2.Platform, + }, + { + ID: maintained3.ID, + Name: maintained3.Name, + Version: maintained3.Version, + Platform: maintained3.Platform, + }, + } + + // Testing pagination + apps, meta, err := ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 3) + require.Equal(t, expectedApps, apps) + require.False(t, meta.HasNextResults) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{PerPage: 1, IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 1) + require.Equal(t, expectedApps[:1], apps) + require.True(t, meta.HasNextResults) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{PerPage: 1, Page: 1, IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 1) + require.Equal(t, expectedApps[1:2], apps) + require.True(t, meta.HasNextResults) + require.True(t, meta.HasPreviousResults) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{PerPage: 1, Page: 2, IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 1) + require.Equal(t, expectedApps[2:3], apps) + require.False(t, meta.HasNextResults) + require.True(t, meta.HasPreviousResults) + + // + // Test excluding results for existing apps (installers) + + /// Irrelevant package + _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "Irrelevant Software", + TeamID: &team1.ID, + InstallScript: "nothing", + Filename: "foo.pkg", + UserID: user.ID, + Platform: string(fleet.MacOSPlatform), + BundleIdentifier: "irrelevant_1", + }) + require.NoError(t, err) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 3) + require.Equal(t, expectedApps, apps) + + /// Correct package on a different team + _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "Maintained1", + TeamID: &team2.ID, + InstallScript: "nothing", + Filename: "foo.pkg", + UserID: user.ID, + Platform: string(fleet.MacOSPlatform), + BundleIdentifier: "fleet.maintained1", + }) + require.NoError(t, err) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 3) + require.Equal(t, expectedApps, apps) + + /// Correct package on the right team with the wrong platform + _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "Maintained1", + TeamID: &team1.ID, + InstallScript: "nothing", + Filename: "foo.pkg", + UserID: user.ID, + Platform: string(fleet.IOSPlatform), + BundleIdentifier: "fleet.maintained1", + }) + require.NoError(t, err) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 3) + require.Equal(t, expectedApps, apps) + + /// Correct team and platform + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, "UPDATE software_installers SET platform = ? WHERE platform = ?", fleet.MacOSPlatform, fleet.IOSPlatform) + return err + }) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 2) + require.Equal(t, expectedApps[1:], apps) + + // + // Test excluding results for existing apps (VPP) + + test.CreateInsertGlobalVPPToken(t, ds) + + // irrelevant vpp app + vppIrrelevant := &fleet.VPPApp{ + Name: "irrelevant_app", + VPPAppTeam: fleet.VPPAppTeam{ + VPPAppID: fleet.VPPAppID{ + AdamID: "1", + Platform: fleet.MacOSPlatform, + }, + }, + BundleIdentifier: "irrelevant_2", + } + _, err = ds.InsertVPPAppWithTeam(ctx, vppIrrelevant, &team1.ID) + require.NoError(t, err) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 2) + require.Equal(t, expectedApps[1:], apps) + + // right vpp app, wrong team + vppMaintained2 := &fleet.VPPApp{ + Name: "Maintained 2", + VPPAppTeam: fleet.VPPAppTeam{ + VPPAppID: fleet.VPPAppID{ + AdamID: "2", + Platform: fleet.MacOSPlatform, + }, + }, + BundleIdentifier: "fleet.maintained2", + } + _, err = ds.InsertVPPAppWithTeam(ctx, vppMaintained2, &team2.ID) + require.NoError(t, err) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 2) + require.Equal(t, expectedApps[1:], apps) + + // right vpp app, right team + _, err = ds.InsertVPPAppWithTeam(ctx, vppMaintained2, &team1.ID) + require.NoError(t, err) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 1) + require.Equal(t, expectedApps[2:], apps) + + // right app, right team, wrong platform + vppMaintained3 := &fleet.VPPApp{ + Name: "Maintained 3", + VPPAppTeam: fleet.VPPAppTeam{ + VPPAppID: fleet.VPPAppID{ + AdamID: "3", + Platform: fleet.IOSPlatform, + }, + }, + BundleIdentifier: "fleet.maintained3", + } + + _, err = ds.InsertVPPAppWithTeam(ctx, vppMaintained3, &team1.ID) + require.NoError(t, err) + + apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, team1.ID, fleet.ListOptions{IncludeMetadata: true}) + require.NoError(t, err) + require.Len(t, apps, 1) + require.Equal(t, expectedApps[2:], apps) +} + func testGetMaintainedAppByID(t *testing.T, ds *Datastore) { ctx := context.Background() diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 40f34363d487..0500471dcda9 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -602,6 +602,10 @@ type Datastore interface { // the given team. UploadedSoftwareExists(ctx context.Context, bundleIdentifier string, teamID *uint) (bool, error) + /////////////////////////////////////////////////////////////////////////////// + // Fleet Managed Apps + ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt ListOptions) ([]MaintainedApp, *PaginationMetadata, error) + /////////////////////////////////////////////////////////////////////////////// // OperatingSystemsStore diff --git a/server/fleet/service.go b/server/fleet/service.go index 134025d5c1a4..07c50553ac7e 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1119,6 +1119,7 @@ type Service interface { // AddFleetMaintainedApp adds a Fleet-maintained app to the given team. AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript, preInstallQuery, postInstallScript string, selfService bool) error + ListFleetMaintainedApps(ctx context.Context, teamID uint, opts ListOptions) ([]MaintainedApp, *PaginationMetadata, error) // ///////////////////////////////////////////////////////////////////////////// // Maintenance windows diff --git a/server/mdm/maintainedapps/testing_utils.go b/server/mdm/maintainedapps/testing_utils.go index 9c08af04b1a7..3697acfd2891 100644 --- a/server/mdm/maintainedapps/testing_utils.go +++ b/server/mdm/maintainedapps/testing_utils.go @@ -21,7 +21,7 @@ import ( // It returns the expected results of the ingestion as a slice of // fleet.MaintainedApps with only a few fields filled - the result of // unmarshaling the testdata/expected_apps.json file. -func IngestMaintainedApps(t *testing.T, ds fleet.Datastore) []*fleet.MaintainedApp { +func IngestMaintainedApps(t *testing.T, ds fleet.Datastore) []fleet.MaintainedApp { _, filename, _, _ := runtime.Caller(0) base := filepath.Dir(filename) testdataDir := filepath.Join(base, "testdata") @@ -50,7 +50,7 @@ func IngestMaintainedApps(t *testing.T, ds fleet.Datastore) []*fleet.MaintainedA err := Refresh(context.Background(), ds, log.NewNopLogger()) require.NoError(t, err) - var expected []*fleet.MaintainedApp + var expected []fleet.MaintainedApp b, err := os.ReadFile(filepath.Join(testdataDir, "expected_apps.json")) require.NoError(t, err) err = json.Unmarshal(b, &expected) diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index c36698993e4c..bf204d35ac05 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -442,6 +442,8 @@ type SetHostSoftwareInstallResultFunc func(ctx context.Context, result *fleet.Ho type UploadedSoftwareExistsFunc func(ctx context.Context, bundleIdentifier string, teamID *uint) (bool, error) +type ListAvailableFleetMaintainedAppsFunc func(ctx context.Context, teamID uint, opt fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) + type GetHostOperatingSystemFunc func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) type ListOperatingSystemsFunc func(ctx context.Context) ([]fleet.OperatingSystem, error) @@ -1726,6 +1728,9 @@ type DataStore struct { UploadedSoftwareExistsFunc UploadedSoftwareExistsFunc UploadedSoftwareExistsFuncInvoked bool + ListAvailableFleetMaintainedAppsFunc ListAvailableFleetMaintainedAppsFunc + ListAvailableFleetMaintainedAppsFuncInvoked bool + GetHostOperatingSystemFunc GetHostOperatingSystemFunc GetHostOperatingSystemFuncInvoked bool @@ -4181,6 +4186,13 @@ func (s *DataStore) UploadedSoftwareExists(ctx context.Context, bundleIdentifier return s.UploadedSoftwareExistsFunc(ctx, bundleIdentifier, teamID) } +func (s *DataStore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) { + s.mu.Lock() + s.ListAvailableFleetMaintainedAppsFuncInvoked = true + s.mu.Unlock() + return s.ListAvailableFleetMaintainedAppsFunc(ctx, teamID, opt) +} + func (s *DataStore) GetHostOperatingSystem(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { s.mu.Lock() s.GetHostOperatingSystemFuncInvoked = true diff --git a/server/service/handler.go b/server/service/handler.go index fd8bc0dccf1a..2460f2e77c6f 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -389,6 +389,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // Fleet-maintained apps ue.POST("/api/_version_/fleet/software/fleet_maintained_apps", addFleetMaintainedAppEndpoint, addFleetMaintainedAppRequest{}) + ue.GET("/api/_version_/fleet/software/fleet_maintained_apps", listFleetMaintainedApps, listFleetMaintainedAppsRequest{}) // Vulnerabilities ue.GET("/api/_version_/fleet/vulnerabilities", listVulnerabilitiesEndpoint, listVulnerabilitiesRequest{}) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 393d200fb674..e534117dd5ac 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -2,6 +2,7 @@ package service import ( "bytes" + "cmp" "context" "crypto/sha256" "database/sql" @@ -17,6 +18,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "sort" "strconv" "strings" @@ -14257,7 +14259,7 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { s.Do("POST", "/api/latest/fleet/software/fleet_maintained_apps", &addFleetMaintainedAppRequest{AppID: 1}, http.StatusNotFound) // Insert the list of maintained apps - maintainedapps.IngestMaintainedApps(t, s.ds) + expectedApps := maintainedapps.IngestMaintainedApps(t, s.ds) // Edit DB to spoof URLs and SHA256 values so we don't have to actually download the installers mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { @@ -14276,6 +14278,43 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1")}}, http.StatusOK, &newTeamResp) team := newTeamResp.Team + // Check apps returned + var listMAResp listFleetMaintainedAppsResponse + s.DoJSON(http.MethodGet, "/api/latest/fleet/software/fleet_maintained_apps", listFleetMaintainedAppsRequest{}, http.StatusOK, &listMAResp, "team_id", strconv.Itoa(int(team.ID))) + require.Nil(t, listMAResp.Err) + require.False(t, listMAResp.Meta.HasPreviousResults) + require.False(t, listMAResp.Meta.HasNextResults) + require.Len(t, listMAResp.FleetMaintainedApps, len(expectedApps)) + var listAppsNoID []fleet.MaintainedApp + for _, app := range listMAResp.FleetMaintainedApps { + app.ID = 0 + listAppsNoID = append(listAppsNoID, app) + } + slices.SortFunc(listAppsNoID, func(a, b fleet.MaintainedApp) int { + return cmp.Compare(a.Name, b.Name) + }) + slices.SortFunc(expectedApps, func(a, b fleet.MaintainedApp) int { + return cmp.Compare(a.Name, b.Name) + }) + require.Equal(t, expectedApps, listAppsNoID) + + var listMAResp2 listFleetMaintainedAppsResponse + s.DoJSON( + http.MethodGet, + "/api/latest/fleet/software/fleet_maintained_apps", + listFleetMaintainedAppsRequest{}, + http.StatusOK, + &listMAResp2, + "team_id", strconv.Itoa(int(team.ID)), + "per_page", "2", + "page", "2", + ) + require.Nil(t, listMAResp2.Err) + require.True(t, listMAResp2.Meta.HasPreviousResults) + require.True(t, listMAResp2.Meta.HasNextResults) + require.Len(t, listMAResp2.FleetMaintainedApps, 2) + require.Equal(t, listMAResp.FleetMaintainedApps[4:6], listMAResp2.FleetMaintainedApps) + // Add an ingested app to the team var addMAResp addFleetMaintainedAppResponse req := &addFleetMaintainedAppRequest{ @@ -14289,6 +14328,11 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { s.DoJSON("POST", "/api/latest/fleet/software/fleet_maintained_apps", req, http.StatusOK, &addMAResp) require.Nil(t, addMAResp.Err) + s.DoJSON(http.MethodGet, "/api/latest/fleet/software/fleet_maintained_apps", listFleetMaintainedAppsRequest{}, http.StatusOK, &listMAResp, "team_id", strconv.Itoa(int(team.ID))) + require.Nil(t, listMAResp.Err) + require.False(t, listMAResp.Meta.HasPreviousResults) + require.Len(t, listMAResp.FleetMaintainedApps, len(expectedApps)-1) + // Validate software installer fields mapp, err := s.ds.GetMaintainedAppByID(ctx, 1) require.NoError(t, err) diff --git a/server/service/software.go b/server/service/software.go index e589ac7fe679..24a75d40af83 100644 --- a/server/service/software.go +++ b/server/service/software.go @@ -248,3 +248,36 @@ func (svc Service) CountSoftware(ctx context.Context, opt fleet.SoftwareListOpti return svc.ds.CountSoftware(ctx, opt) } + +// Fleet Maintained Apps +type listFleetMaintainedAppsRequest struct { + fleet.ListOptions + TeamID uint `query:"team_id"` +} + +type listFleetMaintainedAppsResponse struct { + FleetMaintainedApps []fleet.MaintainedApp `json:"fleet_maintained_apps"` + Meta *fleet.PaginationMetadata `json:"meta"` + Err error `json:"error,omitempty"` +} + +func (r listFleetMaintainedAppsResponse) error() error { return r.Err } + +func listFleetMaintainedApps(ctx context.Context, request any, svc fleet.Service) (errorer, error) { + req := request.(*listFleetMaintainedAppsRequest) + + req.IncludeMetadata = true + + apps, meta, err := svc.ListFleetMaintainedApps(ctx, req.TeamID, req.ListOptions) + if err != nil { + return listFleetMaintainedAppsResponse{Err: err}, nil + } + + return listFleetMaintainedAppsResponse{FleetMaintainedApps: apps, Meta: meta}, nil +} + +func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) { + svc.authz.SkipAuthorization(ctx) + + return nil, nil, fleet.ErrMissingLicense +}