@@ -10,6 +10,61 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
func (ds *Datastore) ListPendingSoftwareInstalls(ctx context.Context, hostID uint) ([]string, error) {
|
||||
const stmt = `
|
||||
SELECT
|
||||
execution_id
|
||||
FROM
|
||||
host_software_installs
|
||||
WHERE
|
||||
host_id = ?
|
||||
AND
|
||||
install_script_exit_code IS NULL
|
||||
AND
|
||||
pre_install_query_output IS NULL
|
||||
ORDER BY
|
||||
created_at ASC
|
||||
`
|
||||
var results []string
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, stmt, hostID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "list pending software installs")
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetSoftwareInstallDetails(ctx context.Context, executionId string) (*fleet.SoftwareInstallDetails, error) {
|
||||
const stmt = `
|
||||
SELECT
|
||||
hsi.host_id AS host_id,
|
||||
hsi.execution_id AS execution_id,
|
||||
hsi.software_installer_id AS installer_id,
|
||||
si.pre_install_query AS pre_install_condition,
|
||||
inst.contents AS install_script,
|
||||
pisnt.contents AS post_install_script
|
||||
FROM
|
||||
host_software_installs hsi
|
||||
INNER JOIN
|
||||
software_installers si
|
||||
ON hsi.software_installer_id = si.id
|
||||
LEFT OUTER JOIN
|
||||
script_contents inst
|
||||
ON inst.id = si.install_script_content_id
|
||||
LEFT OUTER JOIN
|
||||
script_contents pisnt
|
||||
ON pisnt.id = si.post_install_script_content_id
|
||||
WHERE
|
||||
hsi.execution_id = ?`
|
||||
|
||||
result := &fleet.SoftwareInstallDetails{}
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), result, stmt, executionId); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ctxerr.Wrap(ctx, notFound("SoftwareInstallerDetails").WithName(executionId), "get software installer details")
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "list pending software installs")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) MatchOrCreateSoftwareInstaller(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (uint, error) {
|
||||
titleID, err := ds.getOrGenerateSoftwareInstallerTitleID(ctx, payload.Title, payload.Source)
|
||||
if err != nil {
|
||||
@@ -38,13 +93,13 @@ func (ds *Datastore) MatchOrCreateSoftwareInstaller(ctx context.Context, payload
|
||||
stmt := `
|
||||
INSERT INTO software_installers (
|
||||
team_id,
|
||||
global_or_team_id,
|
||||
title_id,
|
||||
global_or_team_id,
|
||||
title_id,
|
||||
storage_id,
|
||||
filename,
|
||||
filename,
|
||||
version,
|
||||
install_script_content_id,
|
||||
pre_install_query,
|
||||
install_script_content_id,
|
||||
pre_install_query,
|
||||
post_install_script_content_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
@@ -105,9 +160,9 @@ SELECT
|
||||
pre_install_query,
|
||||
post_install_script_content_id,
|
||||
uploaded_at
|
||||
FROM
|
||||
FROM
|
||||
software_installers
|
||||
WHERE
|
||||
WHERE
|
||||
id = ?`
|
||||
|
||||
var dest fleet.SoftwareInstaller
|
||||
|
||||
@@ -2,11 +2,16 @@ package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -17,6 +22,7 @@ func TestSoftwareInstallers(t *testing.T) {
|
||||
name string
|
||||
fn func(t *testing.T, ds *Datastore)
|
||||
}{
|
||||
{"SoftwareInstallerDetails", testListSoftwareInstallerDetails},
|
||||
{"InsertSoftwareInstallRequest", testInsertSoftwareInstallRequest},
|
||||
{"GetSoftwareInstallResults", testGetSoftwareInstallResult},
|
||||
}
|
||||
@@ -29,6 +35,142 @@ func TestSoftwareInstallers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testListSoftwareInstallerDetails(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
|
||||
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now())
|
||||
|
||||
script1, err := insertScriptContents(ctx, "hello", ds.writer(ctx))
|
||||
require.NoError(t, err)
|
||||
script1Id, err := script1.LastInsertId()
|
||||
require.NoError(t, err)
|
||||
|
||||
script2, err := insertScriptContents(ctx, "world", ds.writer(ctx))
|
||||
require.NoError(t, err)
|
||||
script2Id, err := script2.LastInsertId()
|
||||
require.NoError(t, err)
|
||||
|
||||
installer1, err := insertSoftwareInstaller(ctx, ds.writer(ctx), "file1", "1.0", "SELECT 1", "storage1", script1Id, script2Id)
|
||||
require.NoError(t, err)
|
||||
installer1Id, err := installer1.LastInsertId()
|
||||
require.NoError(t, err)
|
||||
|
||||
installer2, err := insertSoftwareInstaller(ctx, ds.writer(ctx), "file2", "2.0", "SELECT 2", "storage2", script2Id, script1Id)
|
||||
require.NoError(t, err)
|
||||
installer2Id, err := installer2.LastInsertId()
|
||||
require.NoError(t, err)
|
||||
|
||||
hostInstall1, err := insertHostSoftwareInstalls(ctx, ds.writer(ctx), host1.ID, "exec1", uint(installer1Id))
|
||||
require.NoError(t, err)
|
||||
_ = hostInstall1
|
||||
|
||||
hostInstall2, err := insertHostSoftwareInstalls(ctx, ds.writer(ctx), host1.ID, "exec2", uint(installer2Id))
|
||||
require.NoError(t, err)
|
||||
_ = hostInstall2
|
||||
|
||||
hostInstall3, err := insertHostSoftwareInstalls(ctx, ds.writer(ctx), host2.ID, "exec3", uint(installer1Id))
|
||||
require.NoError(t, err)
|
||||
_ = hostInstall3
|
||||
|
||||
hostInstall4, err := insertHostSoftwareInstalls(ctx, ds.writer(ctx), host2.ID, "exec4", uint(installer2Id))
|
||||
require.NoError(t, err)
|
||||
hostInstall4Id, err := hostInstall4.LastInsertId()
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = ds.writer(ctx).MustExec("UPDATE host_software_installs SET install_script_exit_code = 0 WHERE id = ?", hostInstall4Id)
|
||||
|
||||
hostInstall5, err := insertHostSoftwareInstalls(ctx, ds.writer(ctx), host2.ID, "exec5", uint(installer2Id))
|
||||
require.NoError(t, err)
|
||||
hostInstall5Id, err := hostInstall5.LastInsertId()
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = ds.writer(ctx).MustExec("UPDATE host_software_installs SET pre_install_query_output = 'output' WHERE id = ?", hostInstall5Id)
|
||||
|
||||
installDetailsList1, err := ds.ListPendingSoftwareInstalls(ctx, host1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(installDetailsList1))
|
||||
|
||||
installDetailsList2, err := ds.ListPendingSoftwareInstalls(ctx, host2.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(installDetailsList2))
|
||||
|
||||
require.Contains(t, installDetailsList1, "exec1")
|
||||
require.Contains(t, installDetailsList1, "exec2")
|
||||
|
||||
require.Contains(t, installDetailsList2, "exec3")
|
||||
|
||||
exec1, err := ds.GetSoftwareInstallDetails(ctx, "exec1")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, host1.ID, exec1.HostID)
|
||||
require.Equal(t, "exec1", exec1.ExecutionID)
|
||||
require.Equal(t, "hello", exec1.InstallScript)
|
||||
require.Equal(t, "world", exec1.PostInstallScript)
|
||||
require.Equal(t, uint(installer1Id), exec1.InstallerID)
|
||||
require.Equal(t, "SELECT 1", exec1.PreInstallCondition)
|
||||
}
|
||||
|
||||
func insertHostSoftwareInstalls(
|
||||
ctx context.Context,
|
||||
tx sqlx.ExtContext,
|
||||
hostId uint,
|
||||
executionId string,
|
||||
softwareInstallerId uint,
|
||||
) (sql.Result, error) {
|
||||
stmt := `
|
||||
INSERT INTO host_software_installs (
|
||||
host_id,
|
||||
execution_id,
|
||||
software_installer_id
|
||||
) VALUES (?, ?, ?)
|
||||
`
|
||||
res, err := tx.ExecContext(ctx, stmt, hostId, executionId, softwareInstallerId)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "inserting host software install")
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func insertSoftwareInstaller(
|
||||
ctx context.Context,
|
||||
tx sqlx.ExtContext,
|
||||
filename,
|
||||
version,
|
||||
preinstallQuery,
|
||||
storageId string,
|
||||
installScriptId,
|
||||
postInstallScriptId int64,
|
||||
) (sql.Result, error) {
|
||||
stmt := `
|
||||
INSERT INTO software_installers (
|
||||
filename,
|
||||
version,
|
||||
pre_install_query,
|
||||
install_script_content_id,
|
||||
post_install_script_content_id,
|
||||
storage_id
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
res, err := tx.ExecContext(ctx,
|
||||
stmt,
|
||||
filename,
|
||||
version,
|
||||
preinstallQuery,
|
||||
installScriptId,
|
||||
postInstallScriptId,
|
||||
storageId,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "inserting software installer")
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func testInsertSoftwareInstallRequest(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
@@ -1467,6 +1467,13 @@ type Datastore interface {
|
||||
// Software installers
|
||||
//
|
||||
|
||||
// GetSoftwareInstallDetails returns details required to fetch and
|
||||
// run software installers
|
||||
GetSoftwareInstallDetails(ctx context.Context, executionId string) (*SoftwareInstallDetails, error)
|
||||
// ListPendingSoftwareInstalls returns a list of software
|
||||
// installer execution IDs that have not yet been run for a given host
|
||||
ListPendingSoftwareInstalls(ctx context.Context, hostID uint) ([]string, error)
|
||||
|
||||
// MatchOrCreateSoftwareInstaller matches or creates a new software installer.
|
||||
MatchOrCreateSoftwareInstaller(ctx context.Context, payload *UploadSoftwareInstallerPayload) (uint, error)
|
||||
|
||||
|
||||
@@ -670,6 +670,11 @@ type Service interface {
|
||||
GetInstaller(ctx context.Context, installer Installer) (io.ReadCloser, int64, error)
|
||||
CheckInstallerExistence(ctx context.Context, installer Installer) error
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Software Installers
|
||||
|
||||
GetSoftwareInstallDetails(ctx context.Context, installUUID string) (*SoftwareInstallDetails, error)
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// Apple MDM
|
||||
|
||||
|
||||
@@ -35,6 +35,24 @@ func (FailingSoftwareInstallerStore) Exists(ctx context.Context, installerID str
|
||||
return false, errors.New("software installer store not properly configured")
|
||||
}
|
||||
|
||||
// SoftwareInstallDetailsResult contains all of the information
|
||||
// required for a client to pull in and install software from the fleet server
|
||||
type SoftwareInstallDetails struct {
|
||||
// HostID is used for authentication on the backend and should not
|
||||
// be passed to the client
|
||||
HostID uint `json:"-" db:"host_id"`
|
||||
// ExecutionID is a unique identifier for this installation
|
||||
ExecutionID string `json:"install_id" db:"execution_id"`
|
||||
// InstallerID is the unique identifier for the software package metadata in Fleet.
|
||||
InstallerID uint `json:"installer_id" db:"installer_id"`
|
||||
// PreInstallCondition is the query to run as a condition to installing the software package.
|
||||
PreInstallCondition string `json:"pre_install_condition" db:"pre_install_condition"`
|
||||
// InstallScript is the script to run to install the software package.
|
||||
InstallScript string `json:"install_script" db:"install_script"`
|
||||
// PostInstallScript is the script to run after installing the software package.
|
||||
PostInstallScript string `json:"post_install_script" db:"post_install_script"`
|
||||
}
|
||||
|
||||
// SoftwareInstaller represents a software installer package that can be used to install software on
|
||||
// hosts in Fleet.
|
||||
type SoftwareInstaller struct {
|
||||
|
||||
@@ -925,6 +925,10 @@ type WipeHostViaWindowsMDMFunc func(ctx context.Context, host *fleet.Host, cmd *
|
||||
|
||||
type UpdateHostLockWipeStatusFromAppleMDMResultFunc func(ctx context.Context, hostUUID string, cmdUUID string, requestType string, succeeded bool) error
|
||||
|
||||
type GetSoftwareInstallDetailsFunc func(ctx context.Context, executionId string) (*fleet.SoftwareInstallDetails, error)
|
||||
|
||||
type ListPendingSoftwareInstallsFunc func(ctx context.Context, hostID uint) ([]string, error)
|
||||
|
||||
type MatchOrCreateSoftwareInstallerFunc func(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (uint, error)
|
||||
|
||||
type GetSoftwareInstallerMetadataFunc func(ctx context.Context, id uint) (*fleet.SoftwareInstaller, error)
|
||||
@@ -2293,6 +2297,12 @@ type DataStore struct {
|
||||
UpdateHostLockWipeStatusFromAppleMDMResultFunc UpdateHostLockWipeStatusFromAppleMDMResultFunc
|
||||
UpdateHostLockWipeStatusFromAppleMDMResultFuncInvoked bool
|
||||
|
||||
GetSoftwareInstallDetailsFunc GetSoftwareInstallDetailsFunc
|
||||
GetSoftwareInstallDetailsFuncInvoked bool
|
||||
|
||||
ListPendingSoftwareInstallsFunc ListPendingSoftwareInstallsFunc
|
||||
ListPendingSoftwareInstallsFuncInvoked bool
|
||||
|
||||
MatchOrCreateSoftwareInstallerFunc MatchOrCreateSoftwareInstallerFunc
|
||||
MatchOrCreateSoftwareInstallerFuncInvoked bool
|
||||
|
||||
@@ -5479,6 +5489,20 @@ func (s *DataStore) UpdateHostLockWipeStatusFromAppleMDMResult(ctx context.Conte
|
||||
return s.UpdateHostLockWipeStatusFromAppleMDMResultFunc(ctx, hostUUID, cmdUUID, requestType, succeeded)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetSoftwareInstallDetails(ctx context.Context, executionId string) (*fleet.SoftwareInstallDetails, error) {
|
||||
s.mu.Lock()
|
||||
s.GetSoftwareInstallDetailsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetSoftwareInstallDetailsFunc(ctx, executionId)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListPendingSoftwareInstalls(ctx context.Context, hostID uint) ([]string, error) {
|
||||
s.mu.Lock()
|
||||
s.ListPendingSoftwareInstallsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ListPendingSoftwareInstallsFunc(ctx, hostID)
|
||||
}
|
||||
|
||||
func (s *DataStore) MatchOrCreateSoftwareInstaller(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (uint, error) {
|
||||
s.mu.Lock()
|
||||
s.MatchOrCreateSoftwareInstallerFuncInvoked = true
|
||||
|
||||
@@ -813,6 +813,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
||||
|
||||
oe.POST("/api/fleet/orbit/software_install/package", orbitDownloadSoftwareInstallerEndpoint, orbitDownloadSoftwareInstallerRequest{})
|
||||
|
||||
oe.POST("/api/fleet/orbit/software_install/details", getOrbitSoftwareInstallDetails, orbitGetSoftwareInstallRequest{})
|
||||
|
||||
oeWindowsMDM := oe.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyWindowsMDM())
|
||||
oeWindowsMDM.POST("/api/fleet/orbit/disk_encryption_key", postOrbitDiskEncryptionKeyEndpoint, orbitPostDiskEncryptionKeyRequest{})
|
||||
|
||||
|
||||
@@ -748,6 +748,19 @@ func (svc *Service) SetOrUpdateDiskEncryptionKey(ctx context.Context, encryption
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// Get Orbit pending software installations
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type orbitGetSoftwareInstallRequest struct {
|
||||
OrbitNodeKey string `json:"orbot_node_key"`
|
||||
InstallUUID string `json:"install_uuid"`
|
||||
}
|
||||
|
||||
// interface implementation required by the OrbitClient
|
||||
func (r *orbitGetSoftwareInstallRequest) setOrbitNodeKey(nodeKey string) {
|
||||
r.OrbitNodeKey = nodeKey
|
||||
}
|
||||
|
||||
// Download Orbit software installer request
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -763,6 +776,48 @@ func (r *orbitDownloadSoftwareInstallerRequest) setOrbitNodeKey(nodeKey string)
|
||||
}
|
||||
|
||||
// interface implementation required by orbit authentication
|
||||
func (r *orbitGetSoftwareInstallRequest) orbitHostNodeKey() string {
|
||||
return r.OrbitNodeKey
|
||||
}
|
||||
|
||||
type orbitGetSoftwareInstallResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
*fleet.SoftwareInstallDetails
|
||||
}
|
||||
|
||||
func (r orbitGetSoftwareInstallResponse) error() error { return r.Err }
|
||||
|
||||
func getOrbitSoftwareInstallDetails(ctx context.Context, request any, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*orbitGetSoftwareInstallRequest)
|
||||
details, err := svc.GetSoftwareInstallDetails(ctx, req.InstallUUID)
|
||||
if err != nil {
|
||||
return orbitGetSoftwareInstallResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return orbitGetSoftwareInstallResponse{SoftwareInstallDetails: details}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetSoftwareInstallDetails(ctx context.Context, installUUID string) (*fleet.SoftwareInstallDetails, error) {
|
||||
// this is not a user-authenticated endpoint
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
host, ok := hostctx.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, fleet.OrbitError{Message: "internal error: missing host from request context"}
|
||||
}
|
||||
|
||||
details, err := svc.ds.GetSoftwareInstallDetails(ctx, installUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure it cannot get access to a different host's installers
|
||||
if details.HostID != host.ID {
|
||||
return nil, ctxerr.Wrap(ctx, newNotFoundError(), "no installer found for this host")
|
||||
}
|
||||
return details, nil
|
||||
}
|
||||
|
||||
func (r *orbitDownloadSoftwareInstallerRequest) orbitHostNodeKey() string {
|
||||
return r.OrbitNodeKey
|
||||
}
|
||||
|
||||
@@ -315,3 +315,45 @@ func TestGetOrbitConfigNudge(t *testing.T) {
|
||||
require.True(t, ds.GetHostOperatingSystemFuncInvoked)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSoftwareInstallDetails(t *testing.T) {
|
||||
t.Run("hosts can't get each others installers", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
|
||||
|
||||
ds.GetSoftwareInstallDetailsFunc = func(ctx context.Context, executionId string) (*fleet.SoftwareInstallDetails, error) {
|
||||
return &fleet.SoftwareInstallDetails{
|
||||
HostID: 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
goodCtx := test.HostContext(ctx, &fleet.Host{
|
||||
OsqueryHostID: ptr.String("test"),
|
||||
ID: 1,
|
||||
MDMInfo: &fleet.HostMDM{
|
||||
IsServer: false,
|
||||
InstalledFromDep: true,
|
||||
Enrolled: true,
|
||||
Name: fleet.WellKnownMDMFleet,
|
||||
}})
|
||||
|
||||
badCtx := test.HostContext(ctx, &fleet.Host{
|
||||
OsqueryHostID: ptr.String("test"),
|
||||
ID: 2,
|
||||
MDMInfo: &fleet.HostMDM{
|
||||
IsServer: false,
|
||||
InstalledFromDep: true,
|
||||
Enrolled: true,
|
||||
Name: fleet.WellKnownMDMFleet,
|
||||
}})
|
||||
|
||||
d1, err := svc.GetSoftwareInstallDetails(goodCtx, "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), d1.HostID)
|
||||
|
||||
d2, err := svc.GetSoftwareInstallDetails(badCtx, "")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, d2)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user