mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Revise FMA list endpoint to match Windows FMA spec (#27180)
For #26652. No changes file as that'll come in another PR. Will stack additional PRs on top of this one (for ingestion changes etc.) to get this merged more quickly. # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes - [x] Added/updated automated tests - [x] A detailed QA plan exists on the associated ticket (if it isn't there, work with the product group's QA engineer to add it) - [x] Manual QA for all new/changed functionality
This commit is contained in:
parent
0c77e61cfa
commit
fdff6e16ca
12 changed files with 243 additions and 205 deletions
|
|
@ -57,7 +57,7 @@ func (svc *Service) AddFleetMaintainedApp(
|
|||
return 0, ctxerr.Wrap(ctx, err, "transient server issue validating embedded secrets")
|
||||
}
|
||||
|
||||
app, err := svc.ds.GetMaintainedAppByID(ctx, appID)
|
||||
app, err := svc.ds.GetMaintainedAppByID(ctx, appID, teamID)
|
||||
if err != nil {
|
||||
return 0, ctxerr.Wrap(ctx, err, "getting maintained app by id")
|
||||
}
|
||||
|
|
@ -125,7 +125,7 @@ func (svc *Service) AddFleetMaintainedApp(
|
|||
TeamID: teamID,
|
||||
Version: app.Version,
|
||||
Filename: filename,
|
||||
Platform: string(app.Platform),
|
||||
Platform: app.Platform,
|
||||
Source: "apps",
|
||||
Extension: extension,
|
||||
BundleIdentifier: app.BundleIdentifier,
|
||||
|
|
@ -191,6 +191,7 @@ func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID *uint, o
|
|||
return nil, nil, authErr
|
||||
}
|
||||
|
||||
opts.IncludeMetadata = true
|
||||
avail, meta, err := svc.ds.ListAvailableFleetMaintainedApps(ctx, teamID, opts)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "listing available fleet managed apps")
|
||||
|
|
@ -199,14 +200,20 @@ func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID *uint, o
|
|||
return avail, meta, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetFleetMaintainedApp(ctx context.Context, appID uint) (*fleet.MaintainedApp, error) {
|
||||
// Special case auth for maintained apps (vs. normal installers) as maintained apps are not scoped to a team;
|
||||
// use SoftwareInstaller for authorization elsewhere.
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MaintainedApp{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
func (svc *Service) GetFleetMaintainedApp(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error) {
|
||||
var authErr error
|
||||
// viewing the maintained app without showing team-specific info can be done by anyone who can view individual FMAs
|
||||
if teamID == nil {
|
||||
authErr = svc.authz.Authorize(ctx, &fleet.MaintainedApp{}, fleet.ActionRead)
|
||||
} else { // viewing the maintained app when showing team-specific info requires access to that team
|
||||
authErr = svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionRead)
|
||||
}
|
||||
|
||||
app, err := svc.ds.GetMaintainedAppByID(ctx, appID)
|
||||
if authErr != nil {
|
||||
return nil, authErr
|
||||
}
|
||||
|
||||
app, err := svc.ds.GetMaintainedAppByID(ctx, appID, teamID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get fleet maintained app")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ func TestGetMaintainedAppAuth(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.GetMaintainedAppByIDFunc = func(ctx context.Context, appID uint) (*fleet.MaintainedApp, error) {
|
||||
ds.GetMaintainedAppByIDFunc = func(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error) {
|
||||
return &fleet.MaintainedApp{}, nil
|
||||
}
|
||||
authorizer, err := authz.NewAuthorizer()
|
||||
|
|
@ -122,39 +122,53 @@ func TestGetMaintainedAppAuth(t *testing.T) {
|
|||
svc := &Service{authz: authorizer, ds: ds}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *fleet.User
|
||||
shouldFail bool
|
||||
name string
|
||||
user *fleet.User
|
||||
shouldFailWithNoTeam bool
|
||||
shouldFailWithMatchingTeam bool
|
||||
shouldFailWithDifferentTeam bool
|
||||
}{
|
||||
{
|
||||
"global admin",
|
||||
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"global maintainer",
|
||||
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"global observer",
|
||||
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"team admin",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"team maintainer",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"team observer",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -162,9 +176,25 @@ func TestGetMaintainedAppAuth(t *testing.T) {
|
|||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
|
||||
_, err := svc.GetFleetMaintainedApp(ctx, 123)
|
||||
_, err := svc.GetFleetMaintainedApp(ctx, 123, nil)
|
||||
|
||||
if tt.shouldFail {
|
||||
if tt.shouldFailWithNoTeam {
|
||||
require.Error(t, err)
|
||||
require.ErrorAs(t, err, &forbiddenError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = svc.GetFleetMaintainedApp(ctx, 1, ptr.Uint(1))
|
||||
if tt.shouldFailWithMatchingTeam {
|
||||
require.Error(t, err)
|
||||
require.ErrorAs(t, err, &forbiddenError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = svc.GetFleetMaintainedApp(ctx, 1, ptr.Uint(2))
|
||||
if tt.shouldFailWithDifferentTeam {
|
||||
require.Error(t, err)
|
||||
require.ErrorAs(t, err, &forbiddenError)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -68,28 +69,53 @@ ON DUPLICATE KEY UPDATE
|
|||
return app, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetMaintainedAppByID(ctx context.Context, appID uint) (*fleet.MaintainedApp, error) {
|
||||
const stmt = `
|
||||
SELECT
|
||||
fla.id,
|
||||
fla.name,
|
||||
fla.token,
|
||||
fla.version,
|
||||
fla.platform,
|
||||
fla.installer_url,
|
||||
fla.sha256,
|
||||
fla.bundle_identifier,
|
||||
sc1.contents AS install_script,
|
||||
sc2.contents AS uninstall_script
|
||||
FROM fleet_library_apps fla
|
||||
const teamFMATitlesJoin = `
|
||||
team_titles.id software_title_id FROM fleet_library_apps fla
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT st.id, st.bundle_identifier, st.name
|
||||
FROM software_titles st
|
||||
LEFT JOIN
|
||||
software_installers si
|
||||
ON si.title_id = st.id AND si.global_or_team_id = ?
|
||||
AND si.platform IN ('darwin','windows')
|
||||
LEFT JOIN
|
||||
vpp_apps va
|
||||
ON va.title_id = st.id
|
||||
AND va.platform = 'darwin'
|
||||
LEFT JOIN
|
||||
vpp_apps_teams vat
|
||||
ON vat.adam_id = va.adam_id
|
||||
AND vat.platform = va.platform
|
||||
AND vat.global_or_team_id = ?
|
||||
WHERE si.id IS NOT NULL OR vat.id IS NOT NULL
|
||||
) team_titles ON (
|
||||
team_titles.bundle_identifier != '' AND team_titles.bundle_identifier = fla.bundle_identifier
|
||||
) OR (
|
||||
team_titles.bundle_identifier = '' AND team_titles.name = fla.name
|
||||
)`
|
||||
|
||||
func (ds *Datastore) GetMaintainedAppByID(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error) {
|
||||
stmt := `SELECT fla.id, fla.name, fla.token, fla.version, fla.platform, fla.installer_url, fla.sha256, fla.bundle_identifier,
|
||||
sc1.contents AS install_script, sc2.contents AS uninstall_script, `
|
||||
var args []any
|
||||
|
||||
if teamID != nil {
|
||||
stmt += teamFMATitlesJoin
|
||||
args = []any{teamID, teamID}
|
||||
} else {
|
||||
stmt += `NULL software_title_id FROM fleet_library_apps fla`
|
||||
}
|
||||
|
||||
stmt += `
|
||||
JOIN script_contents sc1 ON sc1.id = fla.install_script_content_id
|
||||
JOIN script_contents sc2 ON sc2.id = fla.uninstall_script_content_id
|
||||
WHERE
|
||||
fla.id = ?
|
||||
`
|
||||
args = append(args, appID)
|
||||
|
||||
var app fleet.MaintainedApp
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &app, stmt, appID); err != nil {
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &app, stmt, args...); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ctxerr.Wrap(ctx, notFound("MaintainedApp"), "no matching maintained app found")
|
||||
}
|
||||
|
|
@ -100,36 +126,30 @@ WHERE
|
|||
return &app, nil
|
||||
}
|
||||
|
||||
// NoMaintainedAppsInDatabase is the error type for no Fleet Maintained Apps in the database
|
||||
type NoMaintainedAppsInDatabase struct {
|
||||
fleet.ErrorWithUUID
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *NoMaintainedAppsInDatabase) Error() string {
|
||||
return `Fleet was unable to ingest the maintained apps list. Run fleetctl trigger name=maintained_apps to try repopulating the apps list.`
|
||||
}
|
||||
|
||||
// StatusCode implements the go-kit http StatusCoder interface.
|
||||
func (e *NoMaintainedAppsInDatabase) StatusCode() int {
|
||||
return http.StatusNotFound
|
||||
}
|
||||
|
||||
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, fla.updated_at FROM fleet_library_apps fla `
|
||||
stmt := `SELECT fla.id, fla.name, fla.platform, `
|
||||
var args []any
|
||||
|
||||
if teamID != nil {
|
||||
stmt += `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.global_or_team_id = ?)
|
||||
OR
|
||||
(va.platform = fla.platform AND vat.global_or_team_id = ?)
|
||||
)
|
||||
)`
|
||||
stmt += teamFMATitlesJoin + ` WHERE TRUE`
|
||||
args = []any{teamID, teamID}
|
||||
} else {
|
||||
stmt += `WHERE TRUE`
|
||||
stmt += `NULL software_title_id FROM fleet_library_apps fla`
|
||||
}
|
||||
|
||||
if match := opt.MatchQuery; match != "" {
|
||||
|
|
@ -138,16 +158,27 @@ func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamI
|
|||
args = append(args, match)
|
||||
}
|
||||
|
||||
// perform a second query to grab the counts. Build the count statement before
|
||||
// perform a second query to grab the filtered count. Build the count statement before
|
||||
// adding the pagination constraints to the stmt but after including the
|
||||
// MatchQuery option sql.
|
||||
dbReader := ds.reader(ctx)
|
||||
getAppsCountStmt := fmt.Sprintf(`SELECT COUNT(DISTINCT s.id) FROM (%s) AS s`, stmt)
|
||||
var counts int
|
||||
if err := sqlx.GetContext(ctx, dbReader, &counts, getAppsCountStmt, args...); err != nil {
|
||||
var filteredCount int
|
||||
if err := sqlx.GetContext(ctx, dbReader, &filteredCount, getAppsCountStmt, args...); err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "get fleet maintained apps count")
|
||||
}
|
||||
|
||||
if filteredCount == 0 { // check if we have nothing in the full apps list, in which case provide an error back
|
||||
var totalCount int
|
||||
if err := sqlx.GetContext(ctx, dbReader, &totalCount, `SELECT COUNT(id) FROM fleet_library_apps`); err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "get fleet maintained apps total count")
|
||||
}
|
||||
|
||||
if totalCount == 0 {
|
||||
return nil, nil, &NoMaintainedAppsInDatabase{}
|
||||
}
|
||||
}
|
||||
|
||||
stmtPaged, args := appendListOptionsWithCursorToSQL(stmt, args, &opt)
|
||||
|
||||
var avail []fleet.MaintainedApp
|
||||
|
|
@ -155,8 +186,8 @@ func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamI
|
|||
return nil, nil, ctxerr.Wrap(ctx, err, "selecting available fleet managed apps")
|
||||
}
|
||||
|
||||
meta := &fleet.PaginationMetadata{HasPreviousResults: opt.Page > 0, TotalResults: uint(counts)} //nolint:gosec // dismiss G115
|
||||
if len(avail) > int(opt.PerPage) { //nolint:gosec // dismiss G115
|
||||
meta := &fleet.PaginationMetadata{HasPreviousResults: opt.Page > 0, TotalResults: uint(filteredCount)} //nolint:gosec // dismiss G115
|
||||
if len(avail) > int(opt.PerPage) { //nolint:gosec // dismiss G115
|
||||
meta.HasNextResults = true
|
||||
avail = avail[:len(avail)-1]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
|
@ -22,8 +23,7 @@ func TestMaintainedApps(t *testing.T) {
|
|||
}{
|
||||
{"UpsertMaintainedApps", testUpsertMaintainedApps},
|
||||
{"IngestWithBrew", testIngestWithBrew},
|
||||
{"ListAvailableApps", testListAvailableApps},
|
||||
{"GetMaintainedAppByID", testGetMaintainedAppByID},
|
||||
{"ListAndGetAvailableApps", testListAndGetAvailableApps},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
|
@ -58,7 +58,7 @@ func testUpsertMaintainedApps(t *testing.T, ds *Datastore) {
|
|||
Token: "figma",
|
||||
InstallerURL: "https://desktop.figma.com/mac-arm/Figma-999.9.9.zip",
|
||||
Version: "999.9.9",
|
||||
Platform: fleet.MacOSPlatform,
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ func testIngestWithBrew(t *testing.T, ds *Datastore) {
|
|||
require.ElementsMatch(t, expectedTokens, actualTokens)
|
||||
}
|
||||
|
||||
func testListAvailableApps(t *testing.T, ds *Datastore) {
|
||||
func testListAndGetAvailableApps(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
user := test.NewUser(t, ds, "Zaphod Beeblebrox", "zaphod@example.com", true)
|
||||
|
|
@ -102,7 +102,7 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
Name: "Maintained1",
|
||||
Token: "maintained1",
|
||||
Version: "1.0.0",
|
||||
Platform: fleet.MacOSPlatform,
|
||||
Platform: "darwin",
|
||||
InstallerURL: "http://example.com/main1",
|
||||
SHA256: "DEADBEEF",
|
||||
BundleIdentifier: "fleet.maintained1",
|
||||
|
|
@ -115,7 +115,7 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
Name: "Maintained2",
|
||||
Token: "maintained2",
|
||||
Version: "1.0.0",
|
||||
Platform: fleet.MacOSPlatform,
|
||||
Platform: "darwin",
|
||||
InstallerURL: "http://example.com/main1",
|
||||
SHA256: "DEADBEEF",
|
||||
BundleIdentifier: "fleet.maintained2",
|
||||
|
|
@ -127,7 +127,7 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
Name: "Maintained3",
|
||||
Token: "maintained3",
|
||||
Version: "1.0.0",
|
||||
Platform: fleet.MacOSPlatform,
|
||||
Platform: "darwin",
|
||||
InstallerURL: "http://example.com/main1",
|
||||
SHA256: "DEADBEEF",
|
||||
BundleIdentifier: "fleet.maintained3",
|
||||
|
|
@ -136,43 +136,37 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
gotApp, err := ds.GetMaintainedAppByID(ctx, maintained1.ID, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, maintained1, gotApp)
|
||||
|
||||
gotApp, err = ds.GetMaintainedAppByID(ctx, maintained1.ID, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, maintained1, gotApp)
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
// We use this assertion for UpdatedAt because we only concerned with
|
||||
// its presence, not its value. We will set it to nil after asserting
|
||||
// to make the expected vs actual comparison easier.
|
||||
assertUpdatedAt := func(apps []fleet.MaintainedApp) {
|
||||
for i, app := range apps {
|
||||
require.NotNil(t, app.UpdatedAt)
|
||||
apps[i].UpdatedAt = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Testing pagination
|
||||
apps, meta, err := ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 3)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps, apps)
|
||||
require.False(t, meta.HasNextResults)
|
||||
|
||||
|
|
@ -180,7 +174,6 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, apps, 1)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps[:1], apps)
|
||||
require.True(t, meta.HasNextResults)
|
||||
|
||||
|
|
@ -188,7 +181,6 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, apps, 1)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps[1:2], apps)
|
||||
require.True(t, meta.HasNextResults)
|
||||
require.True(t, meta.HasPreviousResults)
|
||||
|
|
@ -197,13 +189,12 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, apps, 1)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps[2:3], apps)
|
||||
require.False(t, meta.HasNextResults)
|
||||
require.True(t, meta.HasPreviousResults)
|
||||
|
||||
//
|
||||
// Test excluding results for existing apps (installers)
|
||||
// Test including software title ID for existing apps (installers)
|
||||
|
||||
/// Irrelevant package
|
||||
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
|
|
@ -222,7 +213,6 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, apps, 3)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps, apps)
|
||||
|
||||
/// Correct package on a different team
|
||||
|
|
@ -242,11 +232,10 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, apps, 3)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps, apps)
|
||||
|
||||
/// Correct package on the right team with the wrong platform
|
||||
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
_, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
Title: "Maintained1",
|
||||
TeamID: &team1.ID,
|
||||
InstallScript: "nothing",
|
||||
|
|
@ -262,9 +251,12 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, apps, 3)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps, apps)
|
||||
|
||||
gotApp, err = ds.GetMaintainedAppByID(ctx, maintained1.ID, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, maintained1, gotApp)
|
||||
|
||||
/// 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)
|
||||
|
|
@ -273,13 +265,22 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
|
||||
apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 2)
|
||||
require.EqualValues(t, meta.TotalResults, 2)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps[1:], apps)
|
||||
require.Len(t, apps, 3)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
expectedApps[0].TitleID = ptr.Uint(titleID)
|
||||
require.Equal(t, expectedApps, apps)
|
||||
|
||||
gotApp, err = ds.GetMaintainedAppByID(ctx, maintained1.ID, ptr.Uint(0))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, maintained1, gotApp)
|
||||
|
||||
gotApp, err = ds.GetMaintainedAppByID(ctx, maintained1.ID, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
maintained1.TitleID = ptr.Uint(titleID)
|
||||
require.Equal(t, maintained1, gotApp)
|
||||
|
||||
//
|
||||
// Test excluding results for existing apps (VPP)
|
||||
// Test including software title ID for existing apps (VPP)
|
||||
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
|
||||
|
|
@ -299,10 +300,9 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
|
||||
apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 2)
|
||||
require.EqualValues(t, meta.TotalResults, 2)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps[1:], apps)
|
||||
require.Len(t, apps, 3)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
require.Equal(t, expectedApps, apps)
|
||||
|
||||
// right vpp app, wrong team
|
||||
vppMaintained2 := &fleet.VPPApp{
|
||||
|
|
@ -315,26 +315,14 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
},
|
||||
BundleIdentifier: "fleet.maintained2",
|
||||
}
|
||||
_, err = ds.InsertVPPAppWithTeam(ctx, vppMaintained2, &team2.ID)
|
||||
vppApp, 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.EqualValues(t, meta.TotalResults, 2)
|
||||
assertUpdatedAt(apps)
|
||||
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.EqualValues(t, meta.TotalResults, 1)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps[2:], apps)
|
||||
require.Len(t, apps, 3)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
require.Equal(t, expectedApps, apps)
|
||||
|
||||
// right app, right team, wrong platform
|
||||
vppMaintained3 := &fleet.VPPApp{
|
||||
|
|
@ -353,38 +341,46 @@ func testListAvailableApps(t *testing.T, ds *Datastore) {
|
|||
|
||||
apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 1)
|
||||
require.EqualValues(t, meta.TotalResults, 1)
|
||||
assertUpdatedAt(apps)
|
||||
require.Equal(t, expectedApps[2:], apps)
|
||||
require.Len(t, apps, 3)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
require.Equal(t, expectedApps, apps)
|
||||
|
||||
// viewing with no team selected shouldn't exclude any results
|
||||
gotApp, err = ds.GetMaintainedAppByID(ctx, maintained3.ID, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, maintained3, gotApp)
|
||||
|
||||
// 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, 3)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
expectedApps[1].TitleID = ptr.Uint(vppApp.TitleID)
|
||||
require.Equal(t, expectedApps, apps)
|
||||
|
||||
gotApp, err = ds.GetMaintainedAppByID(ctx, maintained2.ID, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
maintained2.TitleID = ptr.Uint(vppApp.TitleID)
|
||||
require.Equal(t, maintained2, gotApp)
|
||||
|
||||
// viewing with no team selected shouldn't include any title IDs
|
||||
apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, nil, fleet.ListOptions{IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 3)
|
||||
require.EqualValues(t, meta.TotalResults, 3)
|
||||
assertUpdatedAt(apps)
|
||||
expectedApps[0].TitleID = nil
|
||||
expectedApps[1].TitleID = nil
|
||||
require.Equal(t, expectedApps, apps)
|
||||
}
|
||||
|
||||
func testGetMaintainedAppByID(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
expApp, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
|
||||
Name: "foo",
|
||||
Token: "token",
|
||||
Version: "1.0.0",
|
||||
Platform: "darwin",
|
||||
InstallerURL: "https://example.com/foo.zip",
|
||||
SHA256: "sha",
|
||||
BundleIdentifier: "bundle",
|
||||
InstallScript: "install",
|
||||
UninstallScript: "uninstall",
|
||||
})
|
||||
gotApp, err = ds.GetMaintainedAppByID(ctx, maintained1.ID, nil)
|
||||
require.NoError(t, err)
|
||||
maintained1.TitleID = nil
|
||||
require.Equal(t, maintained1, gotApp)
|
||||
|
||||
gotApp, err := ds.GetMaintainedAppByID(ctx, expApp.ID)
|
||||
gotApp, err = ds.GetMaintainedAppByID(ctx, maintained3.ID, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expApp, gotApp)
|
||||
maintained3.TitleID = nil
|
||||
require.Equal(t, maintained3, gotApp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1944,12 +1944,15 @@ type Datastore interface {
|
|||
// Fleet-maintained apps
|
||||
//
|
||||
|
||||
// ListAvailableFleetMaintainedApps returns a list of
|
||||
// Fleet-maintained apps available to a specific team (or the full list of apps if no team is specified)
|
||||
// ListAvailableFleetMaintainedApps returns a list of Fleet-maintained apps, including software title ID if
|
||||
// either the maintained app or a custom package/VPP app for the same app is installed on the specified team,
|
||||
// if a team is specified.
|
||||
ListAvailableFleetMaintainedApps(ctx context.Context, teamID *uint, opt ListOptions) ([]MaintainedApp, *PaginationMetadata, error)
|
||||
|
||||
// GetMaintainedAppByID gets a Fleet-maintained app by its ID.
|
||||
GetMaintainedAppByID(ctx context.Context, appID uint) (*MaintainedApp, error)
|
||||
// GetMaintainedAppByID gets a Fleet-maintained app by its ID, including software title ID if
|
||||
// either the maintained app or a custom package/VPP app for the same app is installed on the specified team,
|
||||
// if a team is specified.
|
||||
GetMaintainedAppByID(ctx context.Context, appID uint, teamID *uint) (*MaintainedApp, error)
|
||||
|
||||
// UpsertMaintainedApp inserts or updates a maintained app using the updated
|
||||
// metadata provided via app.
|
||||
|
|
|
|||
|
|
@ -1,26 +1,23 @@
|
|||
package fleet
|
||||
|
||||
import "time"
|
||||
|
||||
// MaintainedApp represets an app in the Fleet library of maintained apps,
|
||||
// MaintainedApp represents an app in the Fleet library of maintained apps,
|
||||
// as stored in the fleet_library_apps table.
|
||||
type MaintainedApp struct {
|
||||
ID uint `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Token string `json:"-" db:"token"`
|
||||
Version string `json:"version" db:"version"`
|
||||
Platform AppleDevicePlatform `json:"platform" db:"platform"`
|
||||
InstallerURL string `json:"url" db:"installer_url"`
|
||||
SHA256 string `json:"-" db:"sha256"`
|
||||
BundleIdentifier string `json:"-" db:"bundle_identifier"`
|
||||
ID uint `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Token string `json:"-" db:"token"`
|
||||
Version string `json:"version,omitempty" db:"version"`
|
||||
Platform string `json:"platform" db:"platform"`
|
||||
TitleID *uint `json:"software_title_id" db:"software_title_id"`
|
||||
InstallerURL string `json:"url,omitempty" db:"installer_url"`
|
||||
SHA256 string `json:"-" db:"sha256"`
|
||||
BundleIdentifier string `json:"-" db:"bundle_identifier"`
|
||||
|
||||
// InstallScript and UninstallScript are not stored directly in the table, they
|
||||
// must be filled via a JOIN on script_contents. On insert/update/upsert, these
|
||||
// fields are used to provide the content of those scripts.
|
||||
InstallScript string `json:"install_script" db:"install_script"`
|
||||
UninstallScript string `json:"uninstall_script" db:"uninstall_script"`
|
||||
// UpdatedAt is the timestamp when the fleet maintained app data was last updated.
|
||||
UpdatedAt *time.Time `json:"-" db:"updated_at"`
|
||||
InstallScript string `json:"install_script,omitempty" db:"install_script"`
|
||||
UninstallScript string `json:"uninstall_script,omitempty" db:"uninstall_script"`
|
||||
}
|
||||
|
||||
// AuthzType implements authz.AuthzTyper.
|
||||
|
|
|
|||
|
|
@ -1187,10 +1187,10 @@ type Service interface {
|
|||
|
||||
// AddFleetMaintainedApp adds a Fleet-maintained app to the given team.
|
||||
AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript, preInstallQuery, postInstallScript, uninstallScript string, selfService bool, automaticInstall bool, labelsIncludeAny, labelsExcludeAny []string) (uint, error)
|
||||
// ListFleetMaintainedApps lists Fleet-maintained apps available to a specific team
|
||||
// ListFleetMaintainedApps lists Fleet-maintained apps, including associated software title for supplied team ID (if any)
|
||||
ListFleetMaintainedApps(ctx context.Context, teamID *uint, opts ListOptions) ([]MaintainedApp, *PaginationMetadata, error)
|
||||
// GetFleetMaintainedApp returns a Fleet-maintained app by ID
|
||||
GetFleetMaintainedApp(ctx context.Context, appID uint) (*MaintainedApp, error)
|
||||
// GetFleetMaintainedApp returns a Fleet-maintained app by ID, including associated software title for supplied team ID (if any)
|
||||
GetFleetMaintainedApp(ctx context.Context, appID uint, teamID *uint) (*MaintainedApp, error)
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// Maintenance windows
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ func (i ingester) ingestOne(ctx context.Context, app maintainedApp, client *http
|
|||
Token: cask.Token,
|
||||
Version: cask.Version,
|
||||
// for now, maintained apps are always macOS (darwin)
|
||||
Platform: fleet.MacOSPlatform,
|
||||
Platform: "darwin",
|
||||
InstallerURL: cask.URL,
|
||||
SHA256: cask.SHA256,
|
||||
BundleIdentifier: app.BundleIdentifier,
|
||||
|
|
|
|||
|
|
@ -1228,7 +1228,7 @@ type MaybeUpdateSetupExperienceVPPStatusFunc func(ctx context.Context, hostUUID
|
|||
|
||||
type ListAvailableFleetMaintainedAppsFunc func(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error)
|
||||
|
||||
type GetMaintainedAppByIDFunc func(ctx context.Context, appID uint) (*fleet.MaintainedApp, error)
|
||||
type GetMaintainedAppByIDFunc func(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error)
|
||||
|
||||
type UpsertMaintainedAppFunc func(ctx context.Context, app *fleet.MaintainedApp) (*fleet.MaintainedApp, error)
|
||||
|
||||
|
|
@ -7382,11 +7382,11 @@ func (s *DataStore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID
|
|||
return s.ListAvailableFleetMaintainedAppsFunc(ctx, teamID, opt)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetMaintainedAppByID(ctx context.Context, appID uint) (*fleet.MaintainedApp, error) {
|
||||
func (s *DataStore) GetMaintainedAppByID(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error) {
|
||||
s.mu.Lock()
|
||||
s.GetMaintainedAppByIDFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetMaintainedAppByIDFunc(ctx, appID)
|
||||
return s.GetMaintainedAppByIDFunc(ctx, appID, teamID)
|
||||
}
|
||||
|
||||
func (s *DataStore) UpsertMaintainedApp(ctx context.Context, app *fleet.MaintainedApp) (*fleet.MaintainedApp, error) {
|
||||
|
|
|
|||
|
|
@ -367,7 +367,6 @@ 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.PATCH("/api/_version_/fleet/software/fleet_maintained_apps", editFleetMaintainedAppEndpoint, editFleetMaintainedAppRequest{})
|
||||
ue.GET("/api/_version_/fleet/software/fleet_maintained_apps", listFleetMaintainedAppsEndpoint, listFleetMaintainedAppsRequest{})
|
||||
ue.GET("/api/_version_/fleet/software/fleet_maintained_apps/{app_id}", getFleetMaintainedApp, getFleetMaintainedAppRequest{})
|
||||
|
||||
|
|
|
|||
|
|
@ -16165,7 +16165,12 @@ 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
|
||||
expectedApps := maintainedapps.IngestMaintainedApps(t, s.ds)
|
||||
insertedApps := maintainedapps.IngestMaintainedApps(t, s.ds)
|
||||
var expectedApps []fleet.MaintainedApp
|
||||
for _, app := range insertedApps {
|
||||
app.Version = "" // we don't expect version to be returned in list view anymore
|
||||
expectedApps = append(expectedApps, app)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
@ -16199,6 +16204,7 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() {
|
|||
app.ID = 0
|
||||
listAppsNoID = append(listAppsNoID, app)
|
||||
}
|
||||
|
||||
slices.SortFunc(listAppsNoID, func(a, b fleet.MaintainedApp) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
|
@ -16228,7 +16234,7 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() {
|
|||
var getMAResp getFleetMaintainedAppResponse
|
||||
s.DoJSON(http.MethodGet, fmt.Sprintf("/api/latest/fleet/software/fleet_maintained_apps/%d", listMAResp.FleetMaintainedApps[0].ID), getFleetMaintainedAppRequest{}, http.StatusOK, &getMAResp)
|
||||
// TODO this will change when actual install scripts are created.
|
||||
dbAppRecord, err := s.ds.GetMaintainedAppByID(ctx, listMAResp.FleetMaintainedApps[0].ID)
|
||||
dbAppRecord, err := s.ds.GetMaintainedAppByID(ctx, listMAResp.FleetMaintainedApps[0].ID, nil)
|
||||
require.NoError(t, err)
|
||||
dbAppResponse := fleet.MaintainedApp{
|
||||
ID: dbAppRecord.ID,
|
||||
|
|
@ -16309,13 +16315,14 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() {
|
|||
&listMAResp, "team_id", fmt.Sprint(team.ID))
|
||||
require.Nil(t, listMAResp.Err)
|
||||
require.False(t, listMAResp.Meta.HasPreviousResults)
|
||||
require.Len(t, listMAResp.FleetMaintainedApps, len(expectedApps)-1)
|
||||
require.Len(t, listMAResp.FleetMaintainedApps, len(expectedApps))
|
||||
|
||||
// Validate software installer fields
|
||||
mapp, err := s.ds.GetMaintainedAppByID(ctx, 1)
|
||||
mapp, err := s.ds.GetMaintainedAppByID(ctx, 1, &team.ID)
|
||||
require.NoError(t, err)
|
||||
i, err := s.ds.GetSoftwareInstallerMetadataByID(context.Background(), getSoftwareInstallerIDByMAppID(1))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mapp.TitleID, i.TitleID)
|
||||
require.Equal(t, ptr.Uint(1), i.FleetLibraryAppID)
|
||||
require.Equal(t, mapp.SHA256, i.StorageID)
|
||||
require.Equal(t, "darwin", i.Platform)
|
||||
|
|
@ -16393,12 +16400,13 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() {
|
|||
"team_id", "0",
|
||||
)
|
||||
|
||||
mapp, err = s.ds.GetMaintainedAppByID(ctx, 4)
|
||||
mapp, err = s.ds.GetMaintainedAppByID(ctx, 4, ptr.Uint(0))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, resp.Count)
|
||||
title = resp.SoftwareTitles[0]
|
||||
require.NotNil(t, title.BundleIdentifier)
|
||||
require.Equal(t, ptr.String(mapp.BundleIdentifier), title.BundleIdentifier)
|
||||
require.Equal(t, title.ID, *mapp.TitleID)
|
||||
require.Equal(t, mapp.Version, title.SoftwarePackage.Version)
|
||||
require.Equal(t, "installer.zip", title.SoftwarePackage.Name)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/maintainedapps"
|
||||
|
|
@ -64,32 +63,12 @@ func (svc *Service) AddFleetMaintainedApp(ctx context.Context, _ *uint, _ uint,
|
|||
return 0, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
type editFleetMaintainedAppRequest struct {
|
||||
TeamID *uint `json:"team_id"`
|
||||
AppID uint `json:"fleet_maintained_app_id"`
|
||||
InstallScript string `json:"install_script"`
|
||||
PreInstallQuery string `json:"pre_install_query"`
|
||||
PostInstallScript string `json:"post_install_script"`
|
||||
SelfService bool `json:"self_service"`
|
||||
UninstallScript string `json:"uninstall_script"`
|
||||
LabelsIncludeAny []string `json:"labels_include_any"`
|
||||
LabelsExcludeAny []string `json:"labels_exclude_any"`
|
||||
}
|
||||
|
||||
func editFleetMaintainedAppEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
|
||||
// TODO: implement this
|
||||
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
type listFleetMaintainedAppsRequest struct {
|
||||
fleet.ListOptions
|
||||
TeamID *uint `query:"team_id,optional"`
|
||||
}
|
||||
|
||||
type listFleetMaintainedAppsResponse struct {
|
||||
Count int `json:"count"`
|
||||
AppsUpdatedAt *time.Time `json:"apps_updated_at"`
|
||||
FleetMaintainedApps []fleet.MaintainedApp `json:"fleet_maintained_apps"`
|
||||
Meta *fleet.PaginationMetadata `json:"meta"`
|
||||
Err error `json:"error,omitempty"`
|
||||
|
|
@ -100,28 +79,15 @@ func (r listFleetMaintainedAppsResponse) Error() error { return r.Err }
|
|||
func listFleetMaintainedAppsEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.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
|
||||
}
|
||||
|
||||
var latest time.Time
|
||||
for _, app := range apps {
|
||||
if app.UpdatedAt != nil && !app.UpdatedAt.IsZero() && app.UpdatedAt.After(latest) {
|
||||
latest = *app.UpdatedAt
|
||||
}
|
||||
}
|
||||
|
||||
listResp := listFleetMaintainedAppsResponse{
|
||||
FleetMaintainedApps: apps,
|
||||
Count: int(meta.TotalResults), //nolint:gosec // dismiss G115
|
||||
Meta: meta,
|
||||
}
|
||||
if !latest.IsZero() {
|
||||
listResp.AppsUpdatedAt = &latest
|
||||
}
|
||||
|
||||
return listResp, nil
|
||||
}
|
||||
|
|
@ -135,7 +101,8 @@ func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID *uint, o
|
|||
}
|
||||
|
||||
type getFleetMaintainedAppRequest struct {
|
||||
AppID uint `url:"app_id"`
|
||||
AppID uint `url:"app_id"`
|
||||
TeamID *uint `query:"team_id,optional"`
|
||||
}
|
||||
|
||||
type getFleetMaintainedAppResponse struct {
|
||||
|
|
@ -148,7 +115,7 @@ func (r getFleetMaintainedAppResponse) Error() error { return r.Err }
|
|||
func getFleetMaintainedApp(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*getFleetMaintainedAppRequest)
|
||||
|
||||
app, err := svc.GetFleetMaintainedApp(ctx, req.AppID)
|
||||
app, err := svc.GetFleetMaintainedApp(ctx, req.AppID, req.TeamID)
|
||||
if err != nil {
|
||||
return getFleetMaintainedAppResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -156,7 +123,7 @@ func getFleetMaintainedApp(ctx context.Context, request any, svc fleet.Service)
|
|||
return getFleetMaintainedAppResponse{FleetMaintainedApp: app}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetFleetMaintainedApp(ctx context.Context, appID uint) (*fleet.MaintainedApp, error) {
|
||||
func (svc *Service) GetFleetMaintainedApp(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
|
|
|||
Loading…
Reference in a new issue