Skip to content

Commit

Permalink
Final API changes and test updates.
Browse files Browse the repository at this point in the history
  • Loading branch information
getvictor committed Sep 9, 2024
1 parent a429089 commit c39a3fe
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 17 deletions.
152 changes: 152 additions & 0 deletions ee/server/service/software_installers_test.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
21 changes: 21 additions & 0 deletions server/datastore/mysql/scripts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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{
Expand All @@ -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{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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))
}
6 changes: 4 additions & 2 deletions server/datastore/mysql/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions server/datastore/mysql/software_installers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")},
}
Expand Down Expand Up @@ -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)
})
}
Expand Down
12 changes: 8 additions & 4 deletions server/fleet/software_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion server/service/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 23 additions & 7 deletions server/service/integration_enterprise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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"])

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit c39a3fe

Please sign in to comment.