Orbit software info endpoint (#18690)

#18674
This commit is contained in:
Dante Catalfamo
2024-05-06 15:19:45 -04:00
committed by GitHub
parent f85eb0aaff
commit 731ee68a29
9 changed files with 357 additions and 7 deletions
+62 -7
View File
@@ -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()
+7
View File
@@ -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)
+5
View File
@@ -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
+18
View File
@@ -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 {
+24
View File
@@ -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
+2
View File
@@ -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{})
+55
View File
@@ -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
}
+42
View File
@@ -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)
})
}