From eb347231b7c9fe6e4517768df54f592b8ea0748c Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Thu, 12 Sep 2024 17:02:54 -0400 Subject: [PATCH 01/17] Initial outline of list fma --- server/datastore/mysql/fleet_apps.go | 50 ++++++++++++++++++++++++++++ server/fleet/service.go | 2 ++ server/fleet/software.go | 7 ++++ server/service/handler.go | 2 ++ server/service/software.go | 32 ++++++++++++++++++ 5 files changed, 93 insertions(+) create mode 100644 server/datastore/mysql/fleet_apps.go diff --git a/server/datastore/mysql/fleet_apps.go b/server/datastore/mysql/fleet_apps.go new file mode 100644 index 000000000000..9eefa7b6aed9 --- /dev/null +++ b/server/datastore/mysql/fleet_apps.go @@ -0,0 +1,50 @@ +package mysql + +import ( + "context" + "fmt" + + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/jmoiron/sqlx" +) + +func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, page int, pageSize int) ([]fleet.FleetMaintainedAppAvailable, fleet.PaginationMetadata, error) { + stmtSelect := ` +SELECT + id, + name, + version, + platform +FROM + fleet_library_apps fla +WHERE + name NOT IN (%s) +` + + stmtExisting := ` +SELECT + st.name +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 va.adam_id = vat.adam_id ANd va.platform = vat.platform +WHERE + si.global_or_team_id = ? +AND + vat.global_or_team_id = ? +` + stmt := fmt.Sprintf(stmtSelect, stmtExisting) + + var avail []fleet.FleetMaintainedAppAvailable + + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmt, teamID); err != nil { + return nil, fleet.PaginationMetadata{}, ctxerr.Wrap(ctx, err, "selecting available fleet managed apps") + } + + return avail, nil, nil +} diff --git a/server/fleet/service.go b/server/fleet/service.go index a7971cff025b..122fec46905f 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -655,6 +655,8 @@ type Service interface { AddAppStoreApp(ctx context.Context, teamID *uint, appTeam VPPAppTeam) error + ListFleetMaintainedApps(ctx context.Context, teamID uint) ([]FleetMaintainedAppAvailable, PaginationMetadata, error) + // MDMAppleProcessOTAEnrollment handles OTA enrollment requests. // // Per the [spec][1] OTA enrollment is composed of two phases, each diff --git a/server/fleet/software.go b/server/fleet/software.go index 9045d7e37559..a81c29291c23 100644 --- a/server/fleet/software.go +++ b/server/fleet/software.go @@ -436,3 +436,10 @@ type VPPBatchPayloadWithPlatform struct { SelfService bool `json:"self_service"` Platform AppleDevicePlatform `json:"platform"` } + +type FleetMaintainedAppAvailable struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Platform string `json:"platform"` +} diff --git a/server/service/handler.go b/server/service/handler.go index c7b8c11cb4ad..36193db8bb47 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -358,6 +358,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC ue.GET("/api/_version_/fleet/software/versions", listSoftwareVersionsEndpoint, listSoftwareRequest{}) ue.GET("/api/_version_/fleet/software/versions/{id:[0-9]+}", getSoftwareEndpoint, getSoftwareRequest{}) + ue.GET("/api/_version_/fleet/software/fleet_maintained_apps", listFleetMaintainedApps, listFleetMaintainedAppsRequest{}) + // DEPRECATED: use /api/_version_/fleet/software/versions instead ue.GET("/api/_version_/fleet/software", listSoftwareEndpoint, listSoftwareRequest{}) // DEPRECATED: use /api/_version_/fleet/software/versions{id:[0-9]+} instead diff --git a/server/service/software.go b/server/service/software.go index e589ac7fe679..0fa7e6d0d6ba 100644 --- a/server/service/software.go +++ b/server/service/software.go @@ -248,3 +248,35 @@ func (svc Service) CountSoftware(ctx context.Context, opt fleet.SoftwareListOpti return svc.ds.CountSoftware(ctx, opt) } + +// Fleet Maintained Apps +type listFleetMaintainedAppsRequest struct { + TeamID uint `query:"team_id"` + Page int `query:"page"` + PerPage int `query:"per_page"` +} + +type listFleetMaintainedAppsResponse struct { + FleetMaintainedApps []fleet.FleetMaintainedAppAvailable `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) + + apps, meta, err := svc.ListFleetMaintainedApps(ctx, req.TeamID) + if err != nil { + return listFleetMaintainedAppsResponse{Err: err}, nil + } + + return listFleetMaintainedAppsResponse{FleetMaintainedApps: apps, Meta: meta}, nil +} + +func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint) ([]fleet.FleetMaintainedAppAvailable, fleet.PaginationMetadata, error) { + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.PaginationMetadata{}, fleet.ErrMissingLicense +} From 9384161fd99271019dd60053e440faf21fb7449e Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Thu, 12 Sep 2024 17:06:29 -0400 Subject: [PATCH 02/17] Typos --- server/datastore/mysql/fleet_apps.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/datastore/mysql/fleet_apps.go b/server/datastore/mysql/fleet_apps.go index 9eefa7b6aed9..12ae58dfa37b 100644 --- a/server/datastore/mysql/fleet_apps.go +++ b/server/datastore/mysql/fleet_apps.go @@ -9,7 +9,7 @@ import ( "github.com/jmoiron/sqlx" ) -func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, page int, pageSize int) ([]fleet.FleetMaintainedAppAvailable, fleet.PaginationMetadata, error) { +func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, page int, pageSize int) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { stmtSelect := ` SELECT id, @@ -30,7 +30,7 @@ FROM LEFT JOIN software_installers si ON si.title_id = st.id LEFT JOIN - vpp_apps va ON va.title_id - st.id + vpp_apps va ON va.title_id = st.id LEFT JOIN vpp_apps_teams vat ON va.adam_id = vat.adam_id ANd va.platform = vat.platform WHERE @@ -42,8 +42,8 @@ AND var avail []fleet.FleetMaintainedAppAvailable - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmt, teamID); err != nil { - return nil, fleet.PaginationMetadata{}, ctxerr.Wrap(ctx, err, "selecting available fleet managed apps") + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmt, teamID, teamID); err != nil { + return nil, nil, ctxerr.Wrap(ctx, err, "selecting available fleet managed apps") } return avail, nil, nil From 1d1df748980af8f4b047627c61898c4f95451003 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Mon, 16 Sep 2024 15:20:48 -0400 Subject: [PATCH 03/17] Change join format, add pagination, proper service method (no authz) --- ee/server/service/software.go | 12 ++++++++ server/datastore/mysql/fleet_apps.go | 46 +++++++++++++--------------- server/fleet/datastore.go | 4 +++ server/fleet/service.go | 4 +-- server/service/software.go | 9 +++--- 5 files changed, 43 insertions(+), 32 deletions(-) diff --git a/ee/server/service/software.go b/ee/server/service/software.go index 3b66ae485a30..5e229dbe1a9a 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,14 @@ 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.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { + svc.authz.SkipAuthorization(ctx) + + 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/fleet_apps.go b/server/datastore/mysql/fleet_apps.go index 12ae58dfa37b..6e3980861913 100644 --- a/server/datastore/mysql/fleet_apps.go +++ b/server/datastore/mysql/fleet_apps.go @@ -2,49 +2,45 @@ package mysql import ( "context" - "fmt" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/jmoiron/sqlx" ) -func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, page int, pageSize int) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { - stmtSelect := ` +func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt *fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { + stmt := ` SELECT - id, - name, - version, - platform + fla.id, + fla.name, + fla.version, + fla.platform FROM fleet_library_apps fla -WHERE - name NOT IN (%s) -` - - stmtExisting := ` -SELECT - st.name -FROM - software_titles st LEFT JOIN - software_installers si ON si.title_id = st.id + software_titles st ON fla.name = st.name +LEFT JOIN + software_installers si ON si.title_id = st.id AND si.global_or_team_id = ? LEFT JOIN vpp_apps va ON va.title_id = st.id LEFT JOIN - vpp_apps_teams vat ON va.adam_id = vat.adam_id ANd va.platform = vat.platform + vpp_apps_teams vat ON va.adam_id = vat.adam_id AND va.platform = vat.platform AND vat.global_or_team_id = ? WHERE - si.global_or_team_id = ? -AND - vat.global_or_team_id = ? + st.name IS NULL ` - stmt := fmt.Sprintf(stmtSelect, stmtExisting) + stmt, args := appendListOptionsWithCursorToSQL(stmt, []any{teamID, teamID}, opt) var avail []fleet.FleetMaintainedAppAvailable - - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmt, teamID, teamID); err != nil { + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmt, args...); err != nil { return nil, nil, ctxerr.Wrap(ctx, err, "selecting available fleet managed apps") } - return avail, nil, nil + var meta *fleet.PaginationMetadata + 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/fleet/datastore.go b/server/fleet/datastore.go index 5842d8dec1df..6b74e49b7662 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -597,6 +597,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) ([]FleetMaintainedAppAvailable, *PaginationMetadata, error) + /////////////////////////////////////////////////////////////////////////////// // OperatingSystemsStore diff --git a/server/fleet/service.go b/server/fleet/service.go index 122fec46905f..bc768ec99ac9 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1,4 +1,4 @@ -package fleet +.package fleet import ( "context" @@ -655,7 +655,7 @@ type Service interface { AddAppStoreApp(ctx context.Context, teamID *uint, appTeam VPPAppTeam) error - ListFleetMaintainedApps(ctx context.Context, teamID uint) ([]FleetMaintainedAppAvailable, PaginationMetadata, error) + ListFleetMaintainedApps(ctx context.Context, teamID uint, opts ListOptions) ([]FleetMaintainedAppAvailable, PaginationMetadata, error) // MDMAppleProcessOTAEnrollment handles OTA enrollment requests. // diff --git a/server/service/software.go b/server/service/software.go index 0fa7e6d0d6ba..45de2b3c0a84 100644 --- a/server/service/software.go +++ b/server/service/software.go @@ -251,9 +251,8 @@ func (svc Service) CountSoftware(ctx context.Context, opt fleet.SoftwareListOpti // Fleet Maintained Apps type listFleetMaintainedAppsRequest struct { - TeamID uint `query:"team_id"` - Page int `query:"page"` - PerPage int `query:"per_page"` + fleet.ListOptions + TeamID uint `query:"team_id"` } type listFleetMaintainedAppsResponse struct { @@ -267,7 +266,7 @@ func (r listFleetMaintainedAppsResponse) error() error { return r.Err } func listFleetMaintainedApps(ctx context.Context, request any, svc fleet.Service) (errorer, error) { req := request.(listFleetMaintainedAppsRequest) - apps, meta, err := svc.ListFleetMaintainedApps(ctx, req.TeamID) + apps, meta, err := svc.ListFleetMaintainedApps(ctx, req.TeamID, req.ListOptions) if err != nil { return listFleetMaintainedAppsResponse{Err: err}, nil } @@ -275,7 +274,7 @@ func listFleetMaintainedApps(ctx context.Context, request any, svc fleet.Service return listFleetMaintainedAppsResponse{FleetMaintainedApps: apps, Meta: meta}, nil } -func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint) ([]fleet.FleetMaintainedAppAvailable, fleet.PaginationMetadata, error) { +func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, fleet.PaginationMetadata, error) { svc.authz.SkipAuthorization(ctx) return nil, fleet.PaginationMetadata{}, fleet.ErrMissingLicense From b72f6d74fcc5d6101cbefef378438b57f9604115 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Mon, 16 Sep 2024 15:27:07 -0400 Subject: [PATCH 04/17] Typos and interface mismatches --- server/fleet/service.go | 4 ++-- server/service/software.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/fleet/service.go b/server/fleet/service.go index bc768ec99ac9..fde6c29f728d 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1,4 +1,4 @@ -.package fleet +package fleet import ( "context" @@ -655,7 +655,7 @@ type Service interface { AddAppStoreApp(ctx context.Context, teamID *uint, appTeam VPPAppTeam) error - ListFleetMaintainedApps(ctx context.Context, teamID uint, opts ListOptions) ([]FleetMaintainedAppAvailable, PaginationMetadata, error) + ListFleetMaintainedApps(ctx context.Context, teamID uint, opts ListOptions) ([]FleetMaintainedAppAvailable, *PaginationMetadata, error) // MDMAppleProcessOTAEnrollment handles OTA enrollment requests. // diff --git a/server/service/software.go b/server/service/software.go index 45de2b3c0a84..b490a3346657 100644 --- a/server/service/software.go +++ b/server/service/software.go @@ -257,7 +257,7 @@ type listFleetMaintainedAppsRequest struct { type listFleetMaintainedAppsResponse struct { FleetMaintainedApps []fleet.FleetMaintainedAppAvailable `json:"fleet_maintained_apps"` - Meta fleet.PaginationMetadata `json:"meta"` + Meta *fleet.PaginationMetadata `json:"meta"` Err error `json:"error,omitempty"` } @@ -274,8 +274,8 @@ func listFleetMaintainedApps(ctx context.Context, request any, svc fleet.Service return listFleetMaintainedAppsResponse{FleetMaintainedApps: apps, Meta: meta}, nil } -func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, fleet.PaginationMetadata, error) { +func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { svc.authz.SkipAuthorization(ctx) - return nil, fleet.PaginationMetadata{}, fleet.ErrMissingLicense + return nil, nil, fleet.ErrMissingLicense } From a868326a3deba7225c03db9e7cea9d55a96d9be0 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Mon, 16 Sep 2024 15:34:35 -0400 Subject: [PATCH 05/17] Generate mock --- server/mock/datastore_mock.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 6a808dc0ece4..cace1d47fec7 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -438,6 +438,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.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) + type GetHostOperatingSystemFunc func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) type ListOperatingSystemsFunc func(ctx context.Context) ([]fleet.OperatingSystem, error) @@ -1700,6 +1702,9 @@ type DataStore struct { UploadedSoftwareExistsFunc UploadedSoftwareExistsFunc UploadedSoftwareExistsFuncInvoked bool + ListAvailableFleetMaintainedAppsFunc ListAvailableFleetMaintainedAppsFunc + ListAvailableFleetMaintainedAppsFuncInvoked bool + GetHostOperatingSystemFunc GetHostOperatingSystemFunc GetHostOperatingSystemFuncInvoked bool @@ -4117,6 +4122,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.FleetMaintainedAppAvailable, *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 From 3c3939bffa81fea54a776d294e077537be339f19 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Tue, 17 Sep 2024 10:25:41 -0400 Subject: [PATCH 06/17] Move functions to existing files, match platform on join as well --- server/datastore/mysql/fleet_apps.go | 46 ------------------- server/datastore/mysql/maintained_apps.go | 46 +++++++++++++++++++ .../datastore/mysql/maintained_apps_test.go | 42 +++++++++++++++++ 3 files changed, 88 insertions(+), 46 deletions(-) delete mode 100644 server/datastore/mysql/fleet_apps.go diff --git a/server/datastore/mysql/fleet_apps.go b/server/datastore/mysql/fleet_apps.go deleted file mode 100644 index 6e3980861913..000000000000 --- a/server/datastore/mysql/fleet_apps.go +++ /dev/null @@ -1,46 +0,0 @@ -package mysql - -import ( - "context" - - "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" - "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/jmoiron/sqlx" -) - -func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt *fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { - stmt := ` -SELECT - fla.id, - fla.name, - fla.version, - fla.platform -FROM - fleet_library_apps fla -LEFT JOIN - software_titles st ON fla.name = st.name -LEFT JOIN - software_installers si ON si.title_id = st.id AND si.global_or_team_id = ? -LEFT JOIN - vpp_apps va ON va.title_id = st.id -LEFT JOIN - vpp_apps_teams vat ON va.adam_id = vat.adam_id AND va.platform = vat.platform AND vat.global_or_team_id = ? -WHERE - st.name IS NULL -` - stmt, args := appendListOptionsWithCursorToSQL(stmt, []any{teamID, teamID}, opt) - - var avail []fleet.FleetMaintainedAppAvailable - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmt, args...); err != nil { - return nil, nil, ctxerr.Wrap(ctx, err, "selecting available fleet managed apps") - } - - var meta *fleet.PaginationMetadata - 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.go b/server/datastore/mysql/maintained_apps.go index df38f1bcd6f8..d757f441ae50 100644 --- a/server/datastore/mysql/maintained_apps.go +++ b/server/datastore/mysql/maintained_apps.go @@ -52,3 +52,49 @@ ON DUPLICATE KEY UPDATE return ctxerr.Wrap(ctx, err, "upsert maintained app") }) } + +func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt *fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { + stmt := ` +SELECT + fla.id, + fla.name, + fla.version, + fla.platform +FROM + fleet_library_apps fla +LEFT JOIN + software_titles st + ON fla.name = st.name +LEFT JOIN + software_installers si + ON si.title_id = st.id + AND si.platform = fla.platform + AND si.global_or_team_id = ? +LEFT JOIN + vpp_apps va + ON va.title_id = st.id + AND va.platform = fla.platform +LEFT JOIN + vpp_apps_teams vat + ON va.adam_id = vat.adam_id + AND vat.platform = va.platform + AND vat.global_or_team_id = ? +WHERE + st.name IS NULL +` + stmt, args := appendListOptionsWithCursorToSQL(stmt, []any{teamID, teamID}, opt) + + var avail []fleet.FleetMaintainedAppAvailable + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmt, args...); err != nil { + return nil, nil, ctxerr.Wrap(ctx, err, "selecting available fleet managed apps") + } + + var meta *fleet.PaginationMetadata + 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 b011ad8fa2f2..3dda4d7a1e76 100644 --- a/server/datastore/mysql/maintained_apps_test.go +++ b/server/datastore/mysql/maintained_apps_test.go @@ -21,6 +21,7 @@ func TestMaintainedApps(t *testing.T) { }{ {"UpsertMaintainedApps", testUpsertMaintainedApps}, {"IngestWithBrew", testIngestWithBrew}, + {"ListAvailableApps", testListAvailableApps}, } for _, c := range cases { @@ -85,3 +86,44 @@ func testIngestWithBrew(t *testing.T, ds *Datastore) { }) require.ElementsMatch(t, expectedTokens, actualTokens) } + +func testListAvailableApps(t *testing.T, ds *Datastore) { + ctx := context.Background() + 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) + 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) + 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) + +} From e2ee65827f551794814efae38d61c441f84891e2 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Tue, 17 Sep 2024 10:57:11 -0400 Subject: [PATCH 07/17] Updating tests, change function signature --- server/datastore/mysql/maintained_apps.go | 4 +-- .../datastore/mysql/maintained_apps_test.go | 34 +++++++++++++++++-- server/fleet/datastore.go | 2 +- server/fleet/software.go | 8 ++--- server/mock/datastore_mock.go | 4 +-- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/server/datastore/mysql/maintained_apps.go b/server/datastore/mysql/maintained_apps.go index d757f441ae50..de1869277fc8 100644 --- a/server/datastore/mysql/maintained_apps.go +++ b/server/datastore/mysql/maintained_apps.go @@ -53,7 +53,7 @@ ON DUPLICATE KEY UPDATE }) } -func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt *fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { +func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { stmt := ` SELECT fla.id, @@ -82,7 +82,7 @@ LEFT JOIN WHERE st.name IS NULL ` - stmt, args := appendListOptionsWithCursorToSQL(stmt, []any{teamID, teamID}, opt) + stmt, args := appendListOptionsWithCursorToSQL(stmt, []any{teamID, teamID}, &opt) var avail []fleet.FleetMaintainedAppAvailable if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmt, args...); err != nil { diff --git a/server/datastore/mysql/maintained_apps_test.go b/server/datastore/mysql/maintained_apps_test.go index 3dda4d7a1e76..4e5e600fa8c4 100644 --- a/server/datastore/mysql/maintained_apps_test.go +++ b/server/datastore/mysql/maintained_apps_test.go @@ -89,7 +89,11 @@ func testIngestWithBrew(t *testing.T, ds *Datastore) { func testListAvailableApps(t *testing.T, ds *Datastore) { ctx := context.Background() - err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ + + team, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 1"}) + require.NoError(t, err) + + err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ Name: "Maintained1", Token: "maintained1", Version: "1.0.0", @@ -117,7 +121,7 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { Name: "Maintained3", Token: "maintained3", Version: "1.0.0", - Platform: fleet.MacOSPlatform, + Platform: fleet.IOSPlatform, InstallerURL: "http://example.com/main1", SHA256: "DEADBEEF", BundleIdentifier: "fleet.maintained3", @@ -126,4 +130,30 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { }) require.NoError(t, err) + expectedApps := []fleet.FleetMaintainedAppAvailable{ + { + ID: "1", + Name: "Maintained1", + Version: "1.0.0", + Platform: fleet.MacOSPlatform, + }, + { + ID: "2", + Name: "Maintained2", + Version: "1.0.0", + Platform: fleet.MacOSPlatform, + }, + { + ID: "3", + Name: "Maintained3", + Version: "1.0.0", + Platform: fleet.IOSPlatform, + }, + } + + apps, meta, err := ds.ListAvailableFleetMaintainedApps(ctx, team.ID, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, apps, 3) + require.Equal(t, expectedApps, apps) + require.False(t, meta.HasNextResults) } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index b8a72aa3e4aa..0ba246f5b702 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -604,7 +604,7 @@ type Datastore interface { /////////////////////////////////////////////////////////////////////////////// // Fleet Managed Apps - ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt *ListOptions) ([]FleetMaintainedAppAvailable, *PaginationMetadata, error) + ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt ListOptions) ([]FleetMaintainedAppAvailable, *PaginationMetadata, error) /////////////////////////////////////////////////////////////////////////////// // OperatingSystemsStore diff --git a/server/fleet/software.go b/server/fleet/software.go index a81c29291c23..f53803593fdf 100644 --- a/server/fleet/software.go +++ b/server/fleet/software.go @@ -438,8 +438,8 @@ type VPPBatchPayloadWithPlatform struct { } type FleetMaintainedAppAvailable struct { - ID string `json:"id"` - Name string `json:"name"` - Version string `json:"version"` - Platform string `json:"platform"` + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Platform AppleDevicePlatform `json:"platform"` } diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index bc2df4635140..f36b2e92a9e9 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -442,7 +442,7 @@ 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.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) +type ListAvailableFleetMaintainedAppsFunc func(ctx context.Context, teamID uint, opt fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) type GetHostOperatingSystemFunc func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) @@ -4166,7 +4166,7 @@ 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.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { +func (s *DataStore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { s.mu.Lock() s.ListAvailableFleetMaintainedAppsFuncInvoked = true s.mu.Unlock() From 17040bdc85c473d7f3ca0b53adcc8903e139ce5e Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Tue, 17 Sep 2024 17:09:06 -0400 Subject: [PATCH 08/17] Query works, tests work --- ee/server/service/software.go | 2 +- server/datastore/mysql/maintained_apps.go | 50 +++--- .../datastore/mysql/maintained_apps_test.go | 170 +++++++++++++++++- server/service/software.go | 2 + 4 files changed, 195 insertions(+), 29 deletions(-) diff --git a/ee/server/service/software.go b/ee/server/service/software.go index 5e229dbe1a9a..8b842d98c340 100644 --- a/ee/server/service/software.go +++ b/ee/server/service/software.go @@ -21,7 +21,7 @@ func (svc *Service) SoftwareByID(ctx context.Context, id uint, teamID *uint, _ b func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { svc.authz.SkipAuthorization(ctx) - avail, meta, err := svc.ds.ListAvailableFleetMaintainedApps(ctx, teamID, &opts) + avail, meta, err := svc.ds.ListAvailableFleetMaintainedApps(ctx, teamID, opts) if err != nil { return nil, nil, ctxerr.Wrap(ctx, err, "listing available fleet managed apps") } diff --git a/server/datastore/mysql/maintained_apps.go b/server/datastore/mysql/maintained_apps.go index de1869277fc8..aead6e3fc4ea 100644 --- a/server/datastore/mysql/maintained_apps.go +++ b/server/datastore/mysql/maintained_apps.go @@ -62,35 +62,37 @@ SELECT fla.platform FROM fleet_library_apps fla -LEFT JOIN - software_titles st - ON fla.name = st.name -LEFT JOIN - software_installers si - ON si.title_id = st.id - AND si.platform = fla.platform - AND si.global_or_team_id = ? -LEFT JOIN - vpp_apps va - ON va.title_id = st.id - AND va.platform = fla.platform -LEFT JOIN - vpp_apps_teams vat - ON va.adam_id = vat.adam_id - AND vat.platform = va.platform - AND vat.global_or_team_id = ? -WHERE - st.name IS NULL -` - stmt, args := appendListOptionsWithCursorToSQL(stmt, []any{teamID, teamID}, &opt) +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.FleetMaintainedAppAvailable - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmt, args...); err != nil { + 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") } - var meta *fleet.PaginationMetadata - meta = &fleet.PaginationMetadata{HasPreviousResults: opt.Page > 0} + meta := &fleet.PaginationMetadata{HasPreviousResults: opt.Page > 0} if len(avail) > int(opt.PerPage) { meta.HasNextResults = true avail = avail[:len(avail)-1] diff --git a/server/datastore/mysql/maintained_apps_test.go b/server/datastore/mysql/maintained_apps_test.go index 4e5e600fa8c4..03a975cba283 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" @@ -90,7 +91,10 @@ func testIngestWithBrew(t *testing.T, ds *Datastore) { func testListAvailableApps(t *testing.T, ds *Datastore) { ctx := context.Background() - team, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 1"}) + user := test.NewUser(t, ds, "Zaphod Beeblebrox", "zaphod@example.com", true) + team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 1"}) + team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 2"}) + require.NoError(t, err) err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ @@ -121,7 +125,7 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { Name: "Maintained3", Token: "maintained3", Version: "1.0.0", - Platform: fleet.IOSPlatform, + Platform: fleet.MacOSPlatform, InstallerURL: "http://example.com/main1", SHA256: "DEADBEEF", BundleIdentifier: "fleet.maintained3", @@ -147,13 +151,171 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { ID: "3", Name: "Maintained3", Version: "1.0.0", - Platform: fleet.IOSPlatform, + Platform: fleet.MacOSPlatform, }, } - apps, meta, err := ds.ListAvailableFleetMaintainedApps(ctx, team.ID, fleet.ListOptions{}) + // 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) } diff --git a/server/service/software.go b/server/service/software.go index b490a3346657..37b37e6ac543 100644 --- a/server/service/software.go +++ b/server/service/software.go @@ -266,6 +266,8 @@ 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 From 89795339f2701112120fab5c730713cbf190bc58 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Tue, 17 Sep 2024 17:11:12 -0400 Subject: [PATCH 09/17] Add changes/ --- changes/21777-list-fleet-manated-apps | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/21777-list-fleet-manated-apps diff --git a/changes/21777-list-fleet-manated-apps b/changes/21777-list-fleet-manated-apps new file mode 100644 index 000000000000..f94fd567a2fa --- /dev/null +++ b/changes/21777-list-fleet-manated-apps @@ -0,0 +1 @@ +- Add API endpoint to list team available fleet managed apps From 7a53c12103947cd3cfc311aa11793938e21c8233 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Wed, 18 Sep 2024 09:41:45 -0400 Subject: [PATCH 10/17] Feet maintained app id is an integer not a string --- server/datastore/mysql/maintained_apps_test.go | 6 +++--- server/fleet/software.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/datastore/mysql/maintained_apps_test.go b/server/datastore/mysql/maintained_apps_test.go index 03a975cba283..356ff3ec7f23 100644 --- a/server/datastore/mysql/maintained_apps_test.go +++ b/server/datastore/mysql/maintained_apps_test.go @@ -136,19 +136,19 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { expectedApps := []fleet.FleetMaintainedAppAvailable{ { - ID: "1", + ID: 1, Name: "Maintained1", Version: "1.0.0", Platform: fleet.MacOSPlatform, }, { - ID: "2", + ID: 2, Name: "Maintained2", Version: "1.0.0", Platform: fleet.MacOSPlatform, }, { - ID: "3", + ID: 3, Name: "Maintained3", Version: "1.0.0", Platform: fleet.MacOSPlatform, diff --git a/server/fleet/software.go b/server/fleet/software.go index f53803593fdf..d8571ed7c4ad 100644 --- a/server/fleet/software.go +++ b/server/fleet/software.go @@ -438,7 +438,7 @@ type VPPBatchPayloadWithPlatform struct { } type FleetMaintainedAppAvailable struct { - ID string `json:"id"` + ID uint `json:"id"` Name string `json:"name"` Version string `json:"version"` Platform AppleDevicePlatform `json:"platform"` From 3f0bcafa8305d646e27b7b389e7c762ac44ba8bf Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Wed, 18 Sep 2024 09:57:44 -0400 Subject: [PATCH 11/17] Missing err check --- server/datastore/mysql/maintained_apps_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/datastore/mysql/maintained_apps_test.go b/server/datastore/mysql/maintained_apps_test.go index 356ff3ec7f23..e297b8bfcc07 100644 --- a/server/datastore/mysql/maintained_apps_test.go +++ b/server/datastore/mysql/maintained_apps_test.go @@ -93,8 +93,8 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { 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) err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ From 14e7690755b487d2489d74233aba27ac153e5c0b Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:59:32 -0400 Subject: [PATCH 12/17] Change fleet managed to Fleet-maintained --- changes/21777-list-fleet-manated-apps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/21777-list-fleet-manated-apps b/changes/21777-list-fleet-manated-apps index f94fd567a2fa..7da57b618796 100644 --- a/changes/21777-list-fleet-manated-apps +++ b/changes/21777-list-fleet-manated-apps @@ -1 +1 @@ -- Add API endpoint to list team available fleet managed apps +- Add API endpoint to list team available Fleet-maintained apps From d2669a6a28f69610ca1be159974bc5e61f6bad56 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Wed, 18 Sep 2024 14:35:43 -0400 Subject: [PATCH 13/17] Use existing type for maintained apps list, add authz check --- ee/server/service/software.go | 8 ++++++-- server/datastore/mysql/maintained_apps.go | 4 ++-- server/datastore/mysql/maintained_apps_test.go | 2 +- server/fleet/datastore.go | 2 +- server/fleet/service.go | 2 +- server/fleet/software.go | 7 ------- server/mock/datastore_mock.go | 4 ++-- server/service/software.go | 8 ++++---- 8 files changed, 17 insertions(+), 20 deletions(-) diff --git a/ee/server/service/software.go b/ee/server/service/software.go index 8b842d98c340..4bc973af1ee2 100644 --- a/ee/server/service/software.go +++ b/ee/server/service/software.go @@ -18,8 +18,12 @@ func (svc *Service) SoftwareByID(ctx context.Context, id uint, teamID *uint, _ b return svc.Service.SoftwareByID(ctx, id, teamID, true) } -func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { - svc.authz.SkipAuthorization(ctx) +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 { diff --git a/server/datastore/mysql/maintained_apps.go b/server/datastore/mysql/maintained_apps.go index aead6e3fc4ea..24485845166d 100644 --- a/server/datastore/mysql/maintained_apps.go +++ b/server/datastore/mysql/maintained_apps.go @@ -53,7 +53,7 @@ ON DUPLICATE KEY UPDATE }) } -func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { +func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) { stmt := ` SELECT fla.id, @@ -87,7 +87,7 @@ WHERE NOT EXISTS ( stmtPaged, args := appendListOptionsWithCursorToSQL(stmt, []any{teamID, teamID}, &opt) - var avail []fleet.FleetMaintainedAppAvailable + 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") } diff --git a/server/datastore/mysql/maintained_apps_test.go b/server/datastore/mysql/maintained_apps_test.go index e297b8bfcc07..bfede6d98375 100644 --- a/server/datastore/mysql/maintained_apps_test.go +++ b/server/datastore/mysql/maintained_apps_test.go @@ -134,7 +134,7 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { }) require.NoError(t, err) - expectedApps := []fleet.FleetMaintainedAppAvailable{ + expectedApps := []fleet.MaintainedApp{ { ID: 1, Name: "Maintained1", diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 64366469229d..35d236931b40 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -604,7 +604,7 @@ type Datastore interface { /////////////////////////////////////////////////////////////////////////////// // Fleet Managed Apps - ListAvailableFleetMaintainedApps(ctx context.Context, teamID uint, opt ListOptions) ([]FleetMaintainedAppAvailable, *PaginationMetadata, error) + 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 0d832bbfe368..f6748dcd143a 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -658,7 +658,7 @@ type Service interface { AddAppStoreApp(ctx context.Context, teamID *uint, appTeam VPPAppTeam) error - ListFleetMaintainedApps(ctx context.Context, teamID uint, opts ListOptions) ([]FleetMaintainedAppAvailable, *PaginationMetadata, error) + ListFleetMaintainedApps(ctx context.Context, teamID uint, opts ListOptions) ([]MaintainedApp, *PaginationMetadata, error) // MDMAppleProcessOTAEnrollment handles OTA enrollment requests. // diff --git a/server/fleet/software.go b/server/fleet/software.go index d8571ed7c4ad..9045d7e37559 100644 --- a/server/fleet/software.go +++ b/server/fleet/software.go @@ -436,10 +436,3 @@ type VPPBatchPayloadWithPlatform struct { SelfService bool `json:"self_service"` Platform AppleDevicePlatform `json:"platform"` } - -type FleetMaintainedAppAvailable struct { - ID uint `json:"id"` - Name string `json:"name"` - Version string `json:"version"` - Platform AppleDevicePlatform `json:"platform"` -} diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 8e6f5d26eed1..54c58b5a1893 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -442,7 +442,7 @@ 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.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, 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) @@ -4181,7 +4181,7 @@ 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.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { +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() diff --git a/server/service/software.go b/server/service/software.go index 37b37e6ac543..74bcfda214a8 100644 --- a/server/service/software.go +++ b/server/service/software.go @@ -256,9 +256,9 @@ type listFleetMaintainedAppsRequest struct { } type listFleetMaintainedAppsResponse struct { - FleetMaintainedApps []fleet.FleetMaintainedAppAvailable `json:"fleet_maintained_apps"` - Meta *fleet.PaginationMetadata `json:"meta"` - Err error `json:"error,omitempty"` + 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 } @@ -276,7 +276,7 @@ func listFleetMaintainedApps(ctx context.Context, request any, svc fleet.Service return listFleetMaintainedAppsResponse{FleetMaintainedApps: apps, Meta: meta}, nil } -func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]fleet.FleetMaintainedAppAvailable, *fleet.PaginationMetadata, error) { +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 From a516667bf3d6066862456aeb7c91b3e9e7fd6310 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Wed, 18 Sep 2024 15:01:53 -0400 Subject: [PATCH 14/17] Use returned value from upsert now in tests --- .../datastore/mysql/maintained_apps_test.go | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/server/datastore/mysql/maintained_apps_test.go b/server/datastore/mysql/maintained_apps_test.go index 84f80ab0efc4..4825bc8be81f 100644 --- a/server/datastore/mysql/maintained_apps_test.go +++ b/server/datastore/mysql/maintained_apps_test.go @@ -98,7 +98,7 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 2"}) require.NoError(t, err) - err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ + maintained1, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ Name: "Maintained1", Token: "maintained1", Version: "1.0.0", @@ -110,7 +110,7 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { UninstallScript: "echo uninstalled", }) require.NoError(t, err) - err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ + maintained2, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ Name: "Maintained2", Token: "maintained2", Version: "1.0.0", @@ -122,7 +122,7 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { UninstallScript: "echo uninstalled", }) require.NoError(t, err) - err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ + maintained3, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ Name: "Maintained3", Token: "maintained3", Version: "1.0.0", @@ -137,22 +137,22 @@ func testListAvailableApps(t *testing.T, ds *Datastore) { expectedApps := []fleet.MaintainedApp{ { - ID: 1, - Name: "Maintained1", - Version: "1.0.0", - Platform: fleet.MacOSPlatform, + ID: maintained1.ID, + Name: maintained1.Name, + Version: maintained1.Version, + Platform: maintained1.Platform, }, { - ID: 2, - Name: "Maintained2", - Version: "1.0.0", - Platform: fleet.MacOSPlatform, + ID: maintained2.ID, + Name: maintained2.Name, + Version: maintained2.Version, + Platform: maintained2.Platform, }, { - ID: 3, - Name: "Maintained3", - Version: "1.0.0", - Platform: fleet.MacOSPlatform, + ID: maintained3.ID, + Name: maintained3.Name, + Version: maintained3.Version, + Platform: maintained3.Platform, }, } From b81b242c8e3f6228092306aac6369bb6f5bd863f Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Wed, 18 Sep 2024 16:49:52 -0400 Subject: [PATCH 15/17] Add integration test --- server/fleet/service.go | 3 +- server/mdm/maintainedapps/testing_utils.go | 4 +- server/service/integration_enterprise_test.go | 46 ++++++++++++++++++- server/service/software.go | 2 +- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/server/fleet/service.go b/server/fleet/service.go index f0448248e999..07c50553ac7e 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -658,8 +658,6 @@ type Service interface { AddAppStoreApp(ctx context.Context, teamID *uint, appTeam VPPAppTeam) error - ListFleetMaintainedApps(ctx context.Context, teamID uint, opts ListOptions) ([]MaintainedApp, *PaginationMetadata, error) - // MDMAppleProcessOTAEnrollment handles OTA enrollment requests. // // Per the [spec][1] OTA enrollment is composed of two phases, each @@ -1121,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/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 74bcfda214a8..24a75d40af83 100644 --- a/server/service/software.go +++ b/server/service/software.go @@ -264,7 +264,7 @@ type listFleetMaintainedAppsResponse struct { func (r listFleetMaintainedAppsResponse) error() error { return r.Err } func listFleetMaintainedApps(ctx context.Context, request any, svc fleet.Service) (errorer, error) { - req := request.(listFleetMaintainedAppsRequest) + req := request.(*listFleetMaintainedAppsRequest) req.IncludeMetadata = true From b8264f282efa4e7329b57c8497173d19a92d5f4a Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Thu, 19 Sep 2024 09:26:56 -0400 Subject: [PATCH 16/17] Fix other broken test --- server/datastore/mysql/maintained_apps_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/datastore/mysql/maintained_apps_test.go b/server/datastore/mysql/maintained_apps_test.go index 4825bc8be81f..9eb6a6fa0f53 100644 --- a/server/datastore/mysql/maintained_apps_test.go +++ b/server/datastore/mysql/maintained_apps_test.go @@ -37,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") }) @@ -63,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 } } From bf4cd3d7656e7e46d45a66d41386920b094b6715 Mon Sep 17 00:00:00 2001 From: dantecatalfamo Date: Thu, 19 Sep 2024 09:38:10 -0400 Subject: [PATCH 17/17] Move endpoint in handler --- server/service/handler.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/service/handler.go b/server/service/handler.go index 40f773670151..2460f2e77c6f 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -358,8 +358,6 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC ue.GET("/api/_version_/fleet/software/versions", listSoftwareVersionsEndpoint, listSoftwareRequest{}) ue.GET("/api/_version_/fleet/software/versions/{id:[0-9]+}", getSoftwareEndpoint, getSoftwareRequest{}) - ue.GET("/api/_version_/fleet/software/fleet_maintained_apps", listFleetMaintainedApps, listFleetMaintainedAppsRequest{}) - // DEPRECATED: use /api/_version_/fleet/software/versions instead ue.GET("/api/_version_/fleet/software", listSoftwareEndpoint, listSoftwareRequest{}) // DEPRECATED: use /api/_version_/fleet/software/versions{id:[0-9]+} instead @@ -391,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{})