Final API changes and test updates.
This commit is contained in:
@@ -1,13 +1,20 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPreProcessUninstallScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
var input = `
|
||||
blah$PACKAGE_IDS
|
||||
pkgids=$PACKAGE_ID
|
||||
@@ -63,3 +70,148 @@ more (
|
||||
assert.Equal(t, expected, payload.UninstallScript)
|
||||
|
||||
}
|
||||
|
||||
func TestInstallUninstallAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := new(mock.Store)
|
||||
svc := newTestService(t, ds)
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{
|
||||
OrbitNodeKey: ptr.String("orbit_key"),
|
||||
Platform: "darwin",
|
||||
TeamID: ptr.Uint(1),
|
||||
}, nil
|
||||
}
|
||||
ds.GetSoftwareInstallerMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint,
|
||||
withScriptContents bool) (*fleet.SoftwareInstaller, error) {
|
||||
return &fleet.SoftwareInstaller{
|
||||
Name: "installer.pkg",
|
||||
Platform: "darwin",
|
||||
TeamID: ptr.Uint(1),
|
||||
}, nil
|
||||
}
|
||||
ds.GetHostLastInstallDataFunc = func(ctx context.Context, hostID uint, installerID uint) (*fleet.HostLastInstallData, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID uint, softwareInstallerID uint, selfService bool) (string,
|
||||
error) {
|
||||
return "request_id", nil
|
||||
}
|
||||
ds.GetAnyScriptContentsFunc = func(ctx context.Context, id uint) ([]byte, error) {
|
||||
return []byte("script"), nil
|
||||
}
|
||||
ds.NewHostScriptExecutionRequestFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult,
|
||||
error) {
|
||||
return &fleet.HostScriptResult{
|
||||
ExecutionID: "execution_id",
|
||||
}, nil
|
||||
}
|
||||
ds.InsertSoftwareUninstallRequestFunc = func(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *fleet.User
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
"global admin",
|
||||
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"global maintainer",
|
||||
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"global observer",
|
||||
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"team admin",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"team maintainer",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"team observer",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
|
||||
checkAuthErr(t, tt.shouldFail, svc.InstallSoftwareTitle(ctx, 1, 10))
|
||||
checkAuthErr(t, tt.shouldFail, svc.UninstallSoftwareTitle(ctx, 1, 10))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUninstallSoftwareTitle is mostly tested in enterprise integration test. This test hits a few edge cases.
|
||||
func TestUninstallSoftwareTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := new(mock.Store)
|
||||
svc := newTestService(t, ds)
|
||||
|
||||
host := &fleet.Host{
|
||||
OrbitNodeKey: ptr.String("orbit_key"),
|
||||
Platform: "darwin",
|
||||
TeamID: ptr.Uint(1),
|
||||
}
|
||||
|
||||
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
return host, nil
|
||||
}
|
||||
|
||||
// Scripts disabled
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
ServerSettings: fleet.ServerSettings{
|
||||
ScriptsDisabled: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
require.ErrorContains(t, svc.UninstallSoftwareTitle(context.Background(), 1, 10), fleet.RunScriptScriptsDisabledGloballyErrMsg)
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
||||
// Host scripts disabled
|
||||
host.ScriptsEnabled = ptr.Bool(false)
|
||||
require.ErrorContains(t, svc.UninstallSoftwareTitle(context.Background(), 1, 10), fleet.RunScriptsOrbitDisabledErrMsg)
|
||||
|
||||
}
|
||||
|
||||
func checkAuthErr(t *testing.T, shouldFail bool, err error) {
|
||||
t.Helper()
|
||||
if shouldFail {
|
||||
require.Error(t, err)
|
||||
var forbiddenError *authz.Forbidden
|
||||
require.ErrorAs(t, err, &forbiddenError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestService(t *testing.T, ds fleet.Datastore) *Service {
|
||||
t.Helper()
|
||||
authorizer, err := authz.NewAuthorizer()
|
||||
require.NoError(t, err)
|
||||
svc := &Service{
|
||||
authz: authorizer,
|
||||
ds: ds,
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ func TestScripts(t *testing.T) {
|
||||
{"TestLockUnlockManually", testLockUnlockManually},
|
||||
{"TestInsertScriptContents", testInsertScriptContents},
|
||||
{"TestCleanupUnusedScriptContents", testCleanupUnusedScriptContents},
|
||||
{"TestGetAnyScriptContents", testGetAnyScriptContents},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
@@ -263,6 +264,8 @@ func testScripts(t *testing.T, ds *Datastore) {
|
||||
// get unknown script contents
|
||||
_, err = ds.GetScriptContents(ctx, 123)
|
||||
require.ErrorAs(t, err, &nfe)
|
||||
_, err = ds.GetAnyScriptContents(ctx, 123)
|
||||
require.ErrorAs(t, err, &nfe)
|
||||
|
||||
// create global scriptGlobal
|
||||
scriptGlobal, err := ds.NewScript(ctx, &fleet.Script{
|
||||
@@ -284,6 +287,9 @@ func testScripts(t *testing.T, ds *Datastore) {
|
||||
contents, err := ds.GetScriptContents(ctx, scriptGlobal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "echo", string(contents))
|
||||
contents, err = ds.GetAnyScriptContents(ctx, scriptGlobal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "echo", string(contents))
|
||||
|
||||
// create team script but team does not exist
|
||||
_, err = ds.NewScript(ctx, &fleet.Script{
|
||||
@@ -317,6 +323,9 @@ func testScripts(t *testing.T, ds *Datastore) {
|
||||
contents, err = ds.GetScriptContents(ctx, scriptTeam.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "echo 'team'", string(contents))
|
||||
contents, err = ds.GetAnyScriptContents(ctx, scriptTeam.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "echo 'team'", string(contents))
|
||||
|
||||
// try to create another team script with the same name
|
||||
_, err = ds.NewScript(ctx, &fleet.Script{
|
||||
@@ -1247,3 +1256,15 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) {
|
||||
require.Len(t, sc, 1)
|
||||
require.Equal(t, md5ChecksumScriptContent(res.ScriptContents), sc[0].Checksum)
|
||||
}
|
||||
|
||||
func testGetAnyScriptContents(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
contents := `echo foobar;`
|
||||
res, err := insertScriptContents(ctx, ds.writer(ctx), contents)
|
||||
require.NoError(t, err)
|
||||
id, _ := res.LastInsertId()
|
||||
|
||||
result, err := ds.GetAnyScriptContents(ctx, uint(id))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, contents, string(result))
|
||||
}
|
||||
|
||||
@@ -447,8 +447,10 @@ func (ds *Datastore) GetSummaryHostSoftwareInstalls(ctx context.Context, install
|
||||
|
||||
stmt := fmt.Sprintf(`
|
||||
SELECT
|
||||
COALESCE(SUM( IF(status = :software_status_pending_install OR status = :software_status_pending_uninstall, 1, 0)), 0) AS pending,
|
||||
COALESCE(SUM( IF(status = :software_status_failed_install OR status = :software_status_failed_uninstall, 1, 0)), 0) AS failed,
|
||||
COALESCE(SUM( IF(status = :software_status_pending_install, 1, 0)), 0) AS pending_install,
|
||||
COALESCE(SUM( IF(status = :software_status_failed_install, 1, 0)), 0) AS failed_install,
|
||||
COALESCE(SUM( IF(status = :software_status_pending_uninstall, 1, 0)), 0) AS pending_uninstall,
|
||||
COALESCE(SUM( IF(status = :software_status_failed_uninstall, 1, 0)), 0) AS failed_uninstall,
|
||||
COALESCE(SUM( IF(status = :software_status_installed, 1, 0)), 0) AS installed
|
||||
FROM (
|
||||
SELECT
|
||||
|
||||
@@ -331,6 +331,10 @@ func testSoftwareInstallRequests(t *testing.T, ds *Datastore) {
|
||||
return nil
|
||||
})
|
||||
|
||||
// Uninstall request with unknown host
|
||||
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, 99999, si.InstallerID)
|
||||
assert.ErrorContains(t, err, "Host")
|
||||
|
||||
userTeamFilter := fleet.TeamFilter{
|
||||
User: &fleet.User{GlobalRole: ptr.String("admin")},
|
||||
}
|
||||
@@ -432,9 +436,11 @@ func testSoftwareInstallRequests(t *testing.T, ds *Datastore) {
|
||||
summary, err := ds.GetSummaryHostSoftwareInstalls(ctx, installerMeta.InstallerID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fleet.SoftwareInstallerStatusSummary{
|
||||
Installed: 1,
|
||||
Pending: 2,
|
||||
Failed: 2,
|
||||
Installed: 1,
|
||||
PendingInstall: 1,
|
||||
FailedInstall: 1,
|
||||
PendingUninstall: 1,
|
||||
FailedUninstall: 1,
|
||||
}, *summary)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -121,10 +121,14 @@ func (s *SoftwareInstaller) AuthzType() string {
|
||||
type SoftwareInstallerStatusSummary struct {
|
||||
// Installed is the number of hosts that have the software package installed.
|
||||
Installed uint `json:"installed" db:"installed"`
|
||||
// Pending is the number of hosts that have the software package pending installation.
|
||||
Pending uint `json:"pending" db:"pending"`
|
||||
// Failed is the number of hosts that have the software package installation failed.
|
||||
Failed uint `json:"failed" db:"failed"`
|
||||
// PendingInstall is the number of hosts that have the software package pending installation.
|
||||
PendingInstall uint `json:"pending_install" db:"pending_install"`
|
||||
// FailedInstall is the number of hosts that have the software package installation failed.
|
||||
FailedInstall uint `json:"failed_install" db:"failed_install"`
|
||||
// PendingUninstall is the number of hosts that have the software package pending installation.
|
||||
PendingUninstall uint `json:"pending_uninstall" db:"pending_uninstall"`
|
||||
// FailedInstall is the number of hosts that have the software package installation failed.
|
||||
FailedUninstall uint `json:"failed_uninstall" db:"failed_uninstall"`
|
||||
}
|
||||
|
||||
// SoftwareInstallerStatus represents the status of a software installer package on a host.
|
||||
|
||||
@@ -378,7 +378,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
||||
getSoftwareInstallerRequest{})
|
||||
ue.POST("/api/_version_/fleet/software/package", uploadSoftwareInstallerEndpoint, uploadSoftwareInstallerRequest{})
|
||||
ue.DELETE("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/available_for_install", deleteSoftwareInstallerEndpoint, deleteSoftwareInstallerRequest{})
|
||||
ue.GET("/api/_version_/fleet/software/install/results/{install_uuid}", getSoftwareInstallResultsEndpoint, getSoftwareInstallResultsRequest{})
|
||||
ue.GET("/api/_version_/fleet/software/install/{install_uuid}/results", getSoftwareInstallResultsEndpoint,
|
||||
getSoftwareInstallResultsRequest{})
|
||||
ue.POST("/api/_version_/fleet/software/batch", batchSetSoftwareInstallersEndpoint, batchSetSoftwareInstallersRequest{})
|
||||
|
||||
// App store software
|
||||
|
||||
@@ -11030,7 +11030,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
||||
installUUID := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID
|
||||
|
||||
gsirr := getSoftwareInstallResultsResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/results/%s", installUUID), nil, http.StatusOK, &gsirr)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/%s/results", installUUID), nil, http.StatusOK, &gsirr)
|
||||
require.NoError(t, gsirr.Err)
|
||||
require.NotNil(t, gsirr.Results)
|
||||
results := gsirr.Results
|
||||
@@ -11099,9 +11099,9 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
||||
require.Equal(t, "ruby.deb", titleResp.SoftwareTitle.SoftwarePackage.Name)
|
||||
require.NotNil(t, titleResp.SoftwareTitle.SoftwarePackage.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerStatusSummary{
|
||||
Installed: 1,
|
||||
Pending: 2,
|
||||
Failed: 1,
|
||||
Installed: 1,
|
||||
PendingInstall: 2,
|
||||
FailedInstall: 1,
|
||||
}, *titleResp.SoftwareTitle.SoftwarePackage.Status)
|
||||
|
||||
// status is reflected in list hosts responses and counts when filtering by software title and status
|
||||
@@ -11234,6 +11234,22 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", h.ID), nil, http.StatusOK, &listUpcomingAct)
|
||||
require.Len(t, listUpcomingAct.Activities, 1)
|
||||
assert.Equal(t, fleet.ActivityTypeUninstalledSoftware{}.ActivityName(), listUpcomingAct.Activities[0].Type)
|
||||
details := make(map[string]interface{}, 5)
|
||||
require.NoError(t, json.Unmarshal(*listUpcomingAct.Activities[0].Details, &details))
|
||||
assert.EqualValues(t, fleet.SoftwareUninstallPending, details["status"])
|
||||
|
||||
// Check that status is reflected in software title response
|
||||
titleResp = getSoftwareTitleResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, "team_id",
|
||||
strconv.Itoa(int(*teamID)))
|
||||
require.NotNil(t, titleResp.SoftwareTitle.SoftwarePackage)
|
||||
assert.Equal(t, "ruby.deb", titleResp.SoftwareTitle.SoftwarePackage.Name)
|
||||
require.NotNil(t, titleResp.SoftwareTitle.SoftwarePackage.Status)
|
||||
assert.Equal(t, fleet.SoftwareInstallerStatusSummary{
|
||||
PendingInstall: 1,
|
||||
FailedInstall: 1,
|
||||
PendingUninstall: 1,
|
||||
}, *titleResp.SoftwareTitle.SoftwarePackage.Status)
|
||||
|
||||
// Another install/uninstall cannot be send once an uninstall is pending
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", h.ID, titleID), nil, http.StatusBadRequest, &resp)
|
||||
@@ -11252,7 +11268,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
||||
"order_direction", "desc")
|
||||
require.NotEmpty(t, activitiesResp.Activities)
|
||||
assert.Equal(t, fleet.ActivityTypeUninstalledSoftware{}.ActivityName(), activitiesResp.Activities[0].Type)
|
||||
details := make(map[string]interface{}, 5)
|
||||
details = make(map[string]interface{}, 5)
|
||||
require.NoError(t, json.Unmarshal(*activitiesResp.Activities[0].Details, &details))
|
||||
assert.Equal(t, "uninstalled", details["status"])
|
||||
|
||||
@@ -11295,7 +11311,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
||||
require.NotNil(t, instResult.HostDeletedAt)
|
||||
|
||||
gsirr = getSoftwareInstallResultsResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/results/%s", installUUID), nil, http.StatusOK, &gsirr)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/%s/results", installUUID), nil, http.StatusOK, &gsirr)
|
||||
require.NoError(t, gsirr.Err)
|
||||
require.NotNil(t, gsirr.Results)
|
||||
results = gsirr.Results
|
||||
@@ -11455,7 +11471,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
||||
}
|
||||
checkResults := func(want result) {
|
||||
var resp getSoftwareInstallResultsResponse
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/install/results/"+want.InstallUUID, nil, http.StatusOK, &resp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/install/%s/results", want.InstallUUID), nil, http.StatusOK, &resp)
|
||||
|
||||
assert.Equal(t, want.HostID, resp.Results.HostID)
|
||||
assert.Equal(t, want.InstallUUID, resp.Results.InstallUUID)
|
||||
|
||||
Reference in New Issue
Block a user