From 9bc0bc94a35bb2fed773df0c286db1845957abdf Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 7 Jan 2025 16:13:11 -0600 Subject: [PATCH] Added integration test. --- server/datastore/s3/software_installer.go | 19 ++ server/mock/datastore.go | 3 +- .../mock/software/software_installer_store.go | 78 +++++++ server/service/integration_install_test.go | 212 ++++++++++++++++++ 4 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 server/mock/software/software_installer_store.go create mode 100644 server/service/integration_install_test.go diff --git a/server/datastore/s3/software_installer.go b/server/datastore/s3/software_installer.go index 03b390a5c1c5..60494d83f411 100644 --- a/server/datastore/s3/software_installer.go +++ b/server/datastore/s3/software_installer.go @@ -24,3 +24,22 @@ func NewSoftwareInstallerStore(config config.S3Config) (*SoftwareInstallerStore, }, }, nil } + +// NewTestSoftwareInstallerStore is used in tests. +func NewTestSoftwareInstallerStore(conf config.S3Config) (*SoftwareInstallerStore, error) { + store := &s3store{ + bucket: "test-bucket", + cloudFrontConfig: &config.S3CloudFrontConfig{ + BaseURL: conf.SoftwareInstallersCloudFrontURL, + SigningPublicKeyID: conf.SoftwareInstallersCloudFrontURLSigningPublicKeyID, + Signer: conf.SoftwareInstallersCloudFrontSigner, + }, + } + return &SoftwareInstallerStore{ + &commonFileStore{ + s3store: store, + pathPrefix: softwareInstallersPrefix, + fileLabel: "software installer", + }, + }, nil +} diff --git a/server/mock/datastore.go b/server/mock/datastore.go index 7254b42aebfa..59cd07ca629f 100644 --- a/server/mock/datastore.go +++ b/server/mock/datastore.go @@ -11,7 +11,8 @@ import ( //go:generate go run ./mockimpl/impl.go -o nanodep/storage.go "s *Storage" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage.AllDEPStorage" //go:generate go run ./mockimpl/impl.go -o mdm/datastore_mdm_mock.go "fs *MDMAppleStore" "fleet.MDMAppleStore" //go:generate go run ./mockimpl/impl.go -o scep/depot.go "d *Depot" "depot.Depot" -//go:generate go run ./mockimpl/impl.go -o mdm/bootstrap_package_store.go "fs *MDMBootstrapPackageStore" "fleet.MDMBootstrapPackageStore" +//go:generate go run ./mockimpl/impl.go -o mdm/bootstrap_package_store.go "s *MDMBootstrapPackageStore" "fleet.MDMBootstrapPackageStore" +//go:generate go run ./mockimpl/impl.go -o software/software_installer_store.go "s *SoftwareInstallerStore" "fleet.SoftwareInstallerStore" var _ fleet.Datastore = (*Store)(nil) diff --git a/server/mock/software/software_installer_store.go b/server/mock/software/software_installer_store.go new file mode 100644 index 000000000000..9901de56c121 --- /dev/null +++ b/server/mock/software/software_installer_store.go @@ -0,0 +1,78 @@ +// Automatically generated by mockimpl. DO NOT EDIT! + +package mock + +import ( + "context" + "io" + "sync" + "time" + + "github.com/fleetdm/fleet/v4/server/fleet" +) + +var _ fleet.SoftwareInstallerStore = (*SoftwareInstallerStore)(nil) + +type GetFunc func(ctx context.Context, installerID string) (io.ReadCloser, int64, error) + +type PutFunc func(ctx context.Context, installerID string, content io.ReadSeeker) error + +type ExistsFunc func(ctx context.Context, installerID string) (bool, error) + +type CleanupFunc func(ctx context.Context, usedInstallerIDs []string, removeCreatedBefore time.Time) (int, error) + +type SignFunc func(ctx context.Context, fileID string) (string, error) + +type SoftwareInstallerStore struct { + GetFunc GetFunc + GetFuncInvoked bool + + PutFunc PutFunc + PutFuncInvoked bool + + ExistsFunc ExistsFunc + ExistsFuncInvoked bool + + CleanupFunc CleanupFunc + CleanupFuncInvoked bool + + SignFunc SignFunc + SignFuncInvoked bool + + mu sync.Mutex +} + +func (s *SoftwareInstallerStore) Get(ctx context.Context, installerID string) (io.ReadCloser, int64, error) { + s.mu.Lock() + s.GetFuncInvoked = true + s.mu.Unlock() + return s.GetFunc(ctx, installerID) +} + +func (s *SoftwareInstallerStore) Put(ctx context.Context, installerID string, content io.ReadSeeker) error { + s.mu.Lock() + s.PutFuncInvoked = true + s.mu.Unlock() + return s.PutFunc(ctx, installerID, content) +} + +func (s *SoftwareInstallerStore) Exists(ctx context.Context, installerID string) (bool, error) { + s.mu.Lock() + s.ExistsFuncInvoked = true + s.mu.Unlock() + return s.ExistsFunc(ctx, installerID) +} + +func (s *SoftwareInstallerStore) Cleanup(ctx context.Context, usedInstallerIDs []string, removeCreatedBefore time.Time) (int, error) { + s.mu.Lock() + s.CleanupFuncInvoked = true + s.mu.Unlock() + return s.CleanupFunc(ctx, usedInstallerIDs, removeCreatedBefore) +} + +func (s *SoftwareInstallerStore) Sign(ctx context.Context, fileID string) (string, error) { + s.mu.Lock() + s.SignFuncInvoked = true + s.mu.Unlock() + return s.SignFunc(ctx, fileID) +} diff --git a/server/service/integration_install_test.go b/server/service/integration_install_test.go new file mode 100644 index 000000000000..a498ecd5ba16 --- /dev/null +++ b/server/service/integration_install_test.go @@ -0,0 +1,212 @@ +package service + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/fleetdm/fleet/v4/server/config" + "github.com/fleetdm/fleet/v4/server/datastore/mysql" + "github.com/fleetdm/fleet/v4/server/datastore/s3" + "github.com/fleetdm/fleet/v4/server/fleet" + software_mock "github.com/fleetdm/fleet/v4/server/mock/software" + "github.com/go-kit/log" + kitlog "github.com/go-kit/log" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +func TestIntegrationsInstall(t *testing.T) { + testingSuite := new(integrationInstallTestSuite) + testingSuite.withServer.s = &testingSuite.Suite + suite.Run(t, testingSuite) +} + +type integrationInstallTestSuite struct { + withServer + suite.Suite + softwareInstallStore *software_mock.SoftwareInstallerStore +} + +func (s *integrationInstallTestSuite) SetupSuite() { + s.withDS.SetupSuite("integrationInstallTestSuite") + + // Create a mock S3 software install store + softwareInstallStore := &software_mock.SoftwareInstallerStore{} + s.softwareInstallStore = softwareInstallStore + + fleetConfig := config.TestConfig() + signer, _ := rsa.GenerateKey(rand.Reader, 2048) + fleetConfig.S3.SoftwareInstallersCloudFrontSigner = signer + installConfig := TestServerOpts{ + License: &fleet.LicenseInfo{ + Tier: fleet.TierPremium, + }, + Logger: log.NewLogfmtLogger(os.Stdout), + EnableCachedDS: true, + SoftwareInstallStore: softwareInstallStore, + FleetConfig: &fleetConfig, + } + if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" { + installConfig.Logger = kitlog.NewNopLogger() + } + users, server := RunServerForTestsWithDS(s.T(), s.ds, &installConfig) + s.server = server + s.users = users + s.token = s.getTestAdminToken() + s.cachedTokens = make(map[string]string) +} + +func (s *integrationInstallTestSuite) TearDownTest() { + s.withServer.commonTearDownTest(s.T()) +} + +// TestSoftwareInstallerSignedURL tests that the software installer signed URL is returned. +// We are +func (s *integrationInstallTestSuite) TestSoftwareInstallerSignedURL() { + t := s.T() + + openFile := func(name string) *os.File { + f, err := os.Open(filepath.Join("testdata", "software-installers", name)) + require.NoError(t, err) + return f + } + + filename := "ruby.deb" + var expectBytes []byte + var expectLen int + f := openFile(filename) + st, err := f.Stat() + require.NoError(t, err) + expectLen = int(st.Size()) + require.Equal(t, expectLen, 11340) + expectBytes = make([]byte, expectLen) + n, err := f.Read(expectBytes) + require.NoError(t, err) + require.Equal(t, n, expectLen) + f.Close() + + // Set up mocks + var myInstallerID string + s.softwareInstallStore.ExistsFunc = func(ctx context.Context, installerID string) (bool, error) { + return installerID == myInstallerID, nil + } + s.softwareInstallStore.PutFunc = func(ctx context.Context, installerID string, content io.ReadSeeker) error { + myInstallerID = installerID + return nil + } + s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string) (string, error) { + return "https://example.com/signed", nil + } + + var createTeamResp teamResponse + s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{ + Name: t.Name(), + }, http.StatusOK, &createTeamResp) + require.NotZero(t, createTeamResp.Team.ID) + + payload := &fleet.UploadSoftwareInstallerPayload{ + TeamID: &createTeamResp.Team.ID, + InstallScript: "another install script", + PreInstallQuery: "another pre install query", + PostInstallScript: "another post install script", + Filename: filename, + // additional fields below are pre-populated so we can re-use the payload later for the test assertions + Title: "ruby", + Version: "1:2.5.1", + Source: "deb_packages", + StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628", + Platform: "linux", + SelfService: true, + } + s.uploadSoftwareInstaller(t, payload, http.StatusOK, "") + + // check the software installer + var id uint + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + return sqlx.GetContext(context.Background(), q, &id, + `SELECT id FROM software_installers WHERE global_or_team_id = ? AND filename = ?`, payload.TeamID, payload.Filename) + }) + require.NotZero(t, id) + + meta, err := s.ds.GetSoftwareInstallerMetadataByID(context.Background(), id) + require.NoError(t, err) + titleID := *meta.TitleID + + // create an orbit host, assign to team + hostInTeam := createOrbitEnrolledHost(t, "linux", "orbit-host-team", s.ds) + require.NoError(t, s.ds.AddHostsToTeam(context.Background(), &createTeamResp.Team.ID, []uint{hostInTeam.ID})) + + // Create a software installation request + s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", hostInTeam.ID, titleID), installSoftwareRequest{}, + http.StatusAccepted) + + // Get the InstallerUUID + var installUUID string + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + return sqlx.GetContext(context.Background(), q, &installUUID, + "SELECT execution_id FROM host_software_installs WHERE host_id = ?", hostInTeam.ID) + }) + + // Fetch installer details + var orbitSoftwareResp orbitGetSoftwareInstallResponse + s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{ + InstallUUID: installUUID, + OrbitNodeKey: *hostInTeam.OrbitNodeKey, + }, http.StatusOK, &orbitSoftwareResp) + assert.Equal(t, meta.InstallerID, orbitSoftwareResp.InstallerID) + require.NotNil(t, orbitSoftwareResp.SoftwareInstallerURL) + assert.Equal(t, "https://example.com/signed", orbitSoftwareResp.SoftwareInstallerURL.URL) + require.Equal(t, filename, orbitSoftwareResp.SoftwareInstallerURL.Filename) + + // Error in signing -- we simply don't return the URL + s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string) (string, error) { + return "", errors.New("error signing") + } + orbitSoftwareResp = orbitGetSoftwareInstallResponse{} + s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{ + InstallUUID: installUUID, + OrbitNodeKey: *hostInTeam.OrbitNodeKey, + }, http.StatusOK, &orbitSoftwareResp) + assert.Equal(t, meta.InstallerID, orbitSoftwareResp.InstallerID) + assert.Nil(t, orbitSoftwareResp.SoftwareInstallerURL) + + // Now test with the real sign function + signer, _ := rsa.GenerateKey(rand.Reader, 2048) + + s3Config := config.S3Config{ + SoftwareInstallersCloudFrontURL: "https://example.cloudfront.net", + SoftwareInstallersCloudFrontURLSigningPublicKeyID: "ABC123XYZ", + SoftwareInstallersCloudFrontSigner: signer, + } + s3Store, err := s3.NewTestSoftwareInstallerStore(s3Config) + require.NoError(t, err) + s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string) (string, error) { + return s3Store.Sign(ctx, fileID) + } + s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{ + InstallUUID: installUUID, + OrbitNodeKey: *hostInTeam.OrbitNodeKey, + }, http.StatusOK, &orbitSoftwareResp) + assert.Equal(t, meta.InstallerID, orbitSoftwareResp.InstallerID) + require.NotNil(t, orbitSoftwareResp.SoftwareInstallerURL) + assert.True(t, + strings.HasPrefix(orbitSoftwareResp.SoftwareInstallerURL.URL, + s3Config.SoftwareInstallersCloudFrontURL+"/software-installers/"+payload.StorageID+"?Expires="), + orbitSoftwareResp.SoftwareInstallerURL.URL) + assert.Contains(t, orbitSoftwareResp.SoftwareInstallerURL.URL, "&Signature=") + assert.Contains(t, orbitSoftwareResp.SoftwareInstallerURL.URL, + "&Key-Pair-Id="+s3Config.SoftwareInstallersCloudFrontURLSigningPublicKeyID) + require.Equal(t, filename, orbitSoftwareResp.SoftwareInstallerURL.Filename) + +}