@@ -17,6 +17,7 @@ import (
17
17
18
18
"github.com/fleetdm/fleet/v4/pkg/file"
19
19
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
20
+ "github.com/fleetdm/fleet/v4/server/authz"
20
21
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
21
22
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
22
23
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
@@ -415,11 +416,12 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw
415
416
if err != nil {
416
417
return ctxerr .Wrapf (ctx , err , "getting last install data for host %d and installer %d" , host .ID , installer .InstallerID )
417
418
}
418
- if lastInstallRequest != nil && lastInstallRequest .Status != nil && * lastInstallRequest .Status == fleet .SoftwareInstallPending {
419
+ if lastInstallRequest != nil && lastInstallRequest .Status != nil &&
420
+ (* lastInstallRequest .Status == fleet .SoftwareInstallPending || * lastInstallRequest .Status == fleet .SoftwareUninstallPending ) {
419
421
return & fleet.BadRequestError {
420
- Message : "Couldn't install software. Host has a pending install request." ,
422
+ Message : "Could not install software. Host has a pending install/uninstall request." ,
421
423
InternalErr : ctxerr .WrapWithData (
422
- ctx , err , "host already has a pending install for this installer" ,
424
+ ctx , err , "host already has a pending install/uninstall for this installer" ,
423
425
map [string ]any {
424
426
"host_id" : host .ID ,
425
427
"software_installer_id" : installer .InstallerID ,
@@ -594,6 +596,151 @@ func (svc *Service) installSoftwareTitleUsingInstaller(ctx context.Context, host
594
596
return ctxerr .Wrap (ctx , err , "inserting software install request" )
595
597
}
596
598
599
+ func (svc * Service ) UninstallSoftwareTitle (ctx context.Context , hostID uint , softwareTitleID uint ) error {
600
+ // First check if scripts are disabled globally. If so, no need for further processing.
601
+ cfg , err := svc .ds .AppConfig (ctx )
602
+ if err != nil {
603
+ svc .authz .SkipAuthorization (ctx )
604
+ return err
605
+ }
606
+
607
+ if cfg .ServerSettings .ScriptsDisabled {
608
+ svc .authz .SkipAuthorization (ctx )
609
+ return fleet .NewUserMessageError (errors .New (fleet .RunScriptScriptsDisabledGloballyErrMsg ), http .StatusForbidden )
610
+ }
611
+
612
+ // we need to use ds.Host because ds.HostLite doesn't return the orbit node key
613
+ host , err := svc .ds .Host (ctx , hostID )
614
+ if err != nil {
615
+ // if error is because the host does not exist, check first if the user
616
+ // had access to install/uninstall software (to prevent leaking valid host ids).
617
+ if fleet .IsNotFound (err ) {
618
+ if err := svc .authz .Authorize (ctx , & fleet.HostSoftwareInstallerResultAuthz {}, fleet .ActionWrite ); err != nil {
619
+ return err
620
+ }
621
+ }
622
+ svc .authz .SkipAuthorization (ctx )
623
+ return ctxerr .Wrap (ctx , err , "get host" )
624
+ }
625
+
626
+ if host .OrbitNodeKey == nil || * host .OrbitNodeKey == "" {
627
+ // fleetd is required to install software so if the host is enrolled via plain osquery we return an error
628
+ svc .authz .SkipAuthorization (ctx )
629
+ return fleet .NewUserMessageError (errors .New ("host does not have fleetd installed" ), http .StatusUnprocessableEntity )
630
+ }
631
+
632
+ // If scripts are disabled (according to the last detail query), we return an error.
633
+ // host.ScriptsEnabled may be nil for older orbit versions.
634
+ if host .ScriptsEnabled != nil && ! * host .ScriptsEnabled {
635
+ svc .authz .SkipAuthorization (ctx )
636
+ return fleet .NewUserMessageError (errors .New (fleet .RunScriptsOrbitDisabledErrMsg ), http .StatusUnprocessableEntity )
637
+ }
638
+
639
+ // authorize with the host's team
640
+ if err := svc .authz .Authorize (ctx , & fleet.HostSoftwareInstallerResultAuthz {HostTeamID : host .TeamID }, fleet .ActionWrite ); err != nil {
641
+ return err
642
+ }
643
+
644
+ installer , err := svc .ds .GetSoftwareInstallerMetadataByTeamAndTitleID (ctx , host .TeamID , softwareTitleID , false )
645
+ if err != nil {
646
+ if fleet .IsNotFound (err ) {
647
+ return & fleet.BadRequestError {
648
+ Message : "Could not uninstall software. Software title is not available for uninstall. Please add software package to install/uninstall." ,
649
+ InternalErr : ctxerr .WrapWithData (
650
+ ctx , err , "couldn't find an installer for software title" ,
651
+ map [string ]any {"host_id" : host .ID , "team_id" : host .TeamID , "title_id" : softwareTitleID },
652
+ ),
653
+ }
654
+ }
655
+ return ctxerr .Wrap (ctx , err , "finding software installer for title" )
656
+ }
657
+
658
+ lastInstallRequest , err := svc .ds .GetHostLastInstallData (ctx , host .ID , installer .InstallerID )
659
+ if err != nil {
660
+ return ctxerr .Wrapf (ctx , err , "getting last install data for host %d and installer %d" , host .ID , installer .InstallerID )
661
+ }
662
+ if lastInstallRequest != nil && lastInstallRequest .Status != nil &&
663
+ (* lastInstallRequest .Status == fleet .SoftwareInstallPending || * lastInstallRequest .Status == fleet .SoftwareUninstallPending ) {
664
+ return & fleet.BadRequestError {
665
+ Message : "Could not uninstall software. Host has a pending install/uninstall request." ,
666
+ InternalErr : ctxerr .WrapWithData (
667
+ ctx , err , "host already has a pending install/uninstall for this installer" ,
668
+ map [string ]any {
669
+ "host_id" : host .ID ,
670
+ "software_installer_id" : installer .InstallerID ,
671
+ "team_id" : host .TeamID ,
672
+ "title_id" : softwareTitleID ,
673
+ },
674
+ ),
675
+ }
676
+ }
677
+
678
+ // Validate platform
679
+ ext := filepath .Ext (installer .Name )
680
+ requiredPlatform := packageExtensionToPlatform (ext )
681
+ if requiredPlatform == "" {
682
+ // this should never happen
683
+ return ctxerr .Errorf (ctx , "software installer has unsupported type %s" , ext )
684
+ }
685
+
686
+ if host .FleetPlatform () != requiredPlatform {
687
+ return & fleet.BadRequestError {
688
+ Message : fmt .Sprintf ("Package (%s) can be uninstalled only on %s hosts." , ext , requiredPlatform ),
689
+ InternalErr : ctxerr .NewWithData (
690
+ ctx , "invalid host platform for requested uninstall" ,
691
+ map [string ]any {"host_id" : host .ID , "team_id" : host .TeamID , "title_id" : installer .TitleID },
692
+ ),
693
+ }
694
+ }
695
+
696
+ // Get the uninstall script and use the standard script infrastructure to run it.
697
+ contents , err := svc .ds .GetAnyScriptContents (ctx , installer .UninstallScriptContentID )
698
+ if err != nil {
699
+ if fleet .IsNotFound (err ) {
700
+ return fleet .NewInvalidArgumentError ("software_title_id" , `No uninstall script exists for the provided "software_title_id".` ).
701
+ WithStatus (http .StatusNotFound )
702
+ }
703
+ return err
704
+ }
705
+
706
+ var teamID uint
707
+ if host .TeamID != nil {
708
+ teamID = * host .TeamID
709
+ }
710
+ // create the script execution request, the host will be notified of the
711
+ // script execution request via the orbit config's Notifications mechanism.
712
+ request := fleet.HostScriptRequestPayload {
713
+ HostID : host .ID ,
714
+ ScriptContents : string (contents ),
715
+ ScriptContentID : installer .UninstallScriptContentID ,
716
+ TeamID : teamID ,
717
+ }
718
+ if ctxUser := authz .UserFromContext (ctx ); ctxUser != nil {
719
+ request .UserID = & ctxUser .ID
720
+ }
721
+ scriptResult , err := svc .ds .NewHostScriptExecutionRequest (ctx , & request )
722
+ if err != nil {
723
+ return ctxerr .Wrap (ctx , err , "create script execution request" )
724
+ }
725
+
726
+ // Update the host software installs table with the uninstall request
727
+ if err = svc .insertSoftwareUninstallRequest (ctx , scriptResult .ExecutionID , host , installer ); err != nil {
728
+ return err
729
+ }
730
+
731
+ // TODO: Add host activity -- pending uninstall request (upcoming)
732
+
733
+ return nil
734
+ }
735
+
736
+ func (svc * Service ) insertSoftwareUninstallRequest (ctx context.Context , executionID string , host * fleet.Host ,
737
+ installer * fleet.SoftwareInstaller ) error {
738
+ if err := svc .ds .InsertSoftwareUninstallRequest (ctx , executionID , host .ID , installer .InstallerID ); err != nil {
739
+ return ctxerr .Wrap (ctx , err , "inserting software uninstall request" )
740
+ }
741
+ return nil
742
+ }
743
+
597
744
func (svc * Service ) GetSoftwareInstallResults (ctx context.Context , resultUUID string ) (* fleet.HostSoftwareInstallerResult , error ) {
598
745
// Basic auth check
599
746
if err := svc .authz .Authorize (ctx , & fleet.Host {}, fleet .ActionList ); err != nil {
0 commit comments