diff --git a/changes/23913-upcoming-activities-handle-scripts b/changes/23913-upcoming-activities-handle-scripts new file mode 100644 index 000000000000..2e4f31c02f68 --- /dev/null +++ b/changes/23913-upcoming-activities-handle-scripts @@ -0,0 +1,5 @@ +* Added script execution to the new `upcoming_activities` table. +* Added software installs to the new `upcoming_activities` table. +* Added vpp apps installs to the new `upcoming_activities` table. +* Updated the list upcoming activities endpoint to use the new `upcoming_activities` table as source of truth. +* Added support to activate the next activity when one is enqueued or when one is completed. diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index d7cfe52ba682..ad82cb364730 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -511,6 +511,7 @@ the way that the Fleet server works. } else { mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger) } + mds.WithPusher(mdmPushService) checkMDMAssets := func(names []fleet.MDMAssetName) (bool, error) { _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), names, nil) diff --git a/ee/server/service/setup_experience.go b/ee/server/service/setup_experience.go index c11b1ac05f92..508597497fa4 100644 --- a/ee/server/service/setup_experience.go +++ b/ee/server/service/setup_experience.go @@ -178,7 +178,10 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string case len(installersPending) > 0: // enqueue installers for _, installer := range installersPending { - installUUID, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, *installer.SoftwareInstallerID, false, nil) + installUUID, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, *installer.SoftwareInstallerID, fleet.HostSoftwareInstallOptions{ + SelfService: false, + ForSetupExperience: true, + }) if err != nil { return false, ctxerr.Wrap(ctx, err, "queueing setup experience install request") } @@ -207,7 +210,10 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string }, } - cmdUUID, err := svc.installSoftwareFromVPP(ctx, host, vppApp, true, false) + cmdUUID, err := svc.installSoftwareFromVPP(ctx, host, vppApp, true, fleet.HostSoftwareInstallOptions{ + SelfService: false, + ForSetupExperience: true, + }) if err != nil { return false, ctxerr.Wrap(ctx, err, "queueing vpp app installation") } @@ -224,9 +230,12 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string return false, ctxerr.Errorf(ctx, "setup experience script missing content id: %d", *script.SetupExperienceScriptID) } req := &fleet.HostScriptRequestPayload{ - HostID: host.ID, - ScriptName: script.Name, - ScriptContentID: *script.ScriptContentID, + HostID: host.ID, + ScriptName: script.Name, + ScriptContentID: *script.ScriptContentID, + // because the script execution request is associated with setup experience, + // it will be enqueued with a higher priority and will run before other + // items in the queue. SetupExperienceScriptID: script.SetupExperienceScriptID, } res, err := svc.ds.NewHostScriptExecutionRequest(ctx, req) diff --git a/ee/server/service/setup_experience_test.go b/ee/server/service/setup_experience_test.go index 444851c49e2b..290b52912023 100644 --- a/ee/server/service/setup_experience_test.go +++ b/ee/server/service/setup_experience_test.go @@ -56,7 +56,7 @@ func TestSetupExperienceNextStep(t *testing.T) { return mockListHostsLite, nil } - ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID, softwareInstallerID uint, selfService bool, policyID *uint) (string, error) { + ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID, softwareInstallerID uint, opts fleet.HostSoftwareInstallOptions) (string, error) { requestedInstalls[hostID] = append(requestedInstalls[hostID], softwareInstallerID) return "install-uuid", nil } diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index 02794196d037..753a73da5c02 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -16,7 +16,6 @@ import ( "github.com/fleetdm/fleet/v4/pkg/file" "github.com/fleetdm/fleet/v4/pkg/fleethttp" - "github.com/fleetdm/fleet/v4/server/authz" authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" hostctx "github.com/fleetdm/fleet/v4/server/contexts/host" @@ -1001,17 +1000,19 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw return ctxerr.Wrap(ctx, err, "finding VPP app for title") } - _, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, false) + _, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, fleet.HostSoftwareInstallOptions{ + SelfService: false, + }) return err } -func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, appleDevice bool, selfService bool) (string, error) { +func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, appleDevice bool, opts fleet.HostSoftwareInstallOptions) (string, error) { token, err := svc.GetVPPTokenIfCanInstallVPPApps(ctx, appleDevice, host) if err != nil { return "", err } - return svc.InstallVPPAppPostValidation(ctx, host, vppApp, token, selfService, nil) + return svc.InstallVPPAppPostValidation(ctx, host, vppApp, token, opts) } func (svc *Service) GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDevice bool, host *fleet.Host) (string, error) { @@ -1057,7 +1058,7 @@ func (svc *Service) GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDev return token, nil } -func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, token string, selfService bool, policyID *uint) (string, error) { +func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, token string, opts fleet.HostSoftwareInstallOptions) (string, error) { // at this moment, neither the UI nor the back-end are prepared to // handle [asyncronous errors][1] on assignment, so before assigning a // device to a license, we need to: @@ -1121,14 +1122,19 @@ func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet } } - // add command to install - cmdUUID := uuid.NewString() - err = svc.mdmAppleCommander.InstallApplication(ctx, []string{host.UUID}, cmdUUID, vppApp.AdamID) - if err != nil { - return "", ctxerr.Wrapf(ctx, err, "sending command to install VPP %s application to host with serial %s", vppApp.AdamID, host.HardwareSerial) - } + // TODO(mna): should we associate the device (give the license) only when the + // upcoming activity is ready to run? I don't think so, because then it could + // fail when it's ready to run which is probably a worse UX as once enqueued + // you expect it to succeed. But eventually, we should do better management + // of the licenses, e.g. if the upcoming activity gets cancelled, it should + // release the reserved license. + // + // But the command is definitely not enqueued now, only when activating the + // activity. - err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, vppApp.VPPAppID, cmdUUID, eventID, selfService, policyID) + // enqueue the VPP app command to install + cmdUUID := uuid.NewString() + err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, vppApp.VPPAppID, cmdUUID, eventID, opts) if err != nil { return "", ctxerr.Wrapf(ctx, err, "inserting host vpp software install for host with serial %s and app with adamID %s", host.HardwareSerial, vppApp.AdamID) } @@ -1154,7 +1160,9 @@ func (svc *Service) installSoftwareTitleUsingInstaller(ctx context.Context, host } } - _, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, installer.InstallerID, false, nil) + _, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, installer.InstallerID, fleet.HostSoftwareInstallOptions{ + SelfService: false, + }) return ctxerr.Wrap(ctx, err, "inserting software install request") } @@ -1244,8 +1252,9 @@ func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, sof } } - // Get the uninstall script and use the standard script infrastructure to run it. - contents, err := svc.ds.GetAnyScriptContents(ctx, installer.UninstallScriptContentID) + // Get the uninstall script to validate there is one, will use the standard + // script infrastructure to run it. + _, err = svc.ds.GetAnyScriptContents(ctx, installer.UninstallScriptContentID) if err != nil { if fleet.IsNotFound(err) { return ctxerr.Wrap(ctx, @@ -1255,32 +1264,11 @@ func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, sof return err } - var teamID uint - if host.TeamID != nil { - teamID = *host.TeamID - } - // create the script execution request; the host will be notified of the - // script execution request via the orbit config's Notifications mechanism. - request := fleet.HostScriptRequestPayload{ - HostID: host.ID, - ScriptContents: string(contents), - ScriptContentID: installer.UninstallScriptContentID, - TeamID: teamID, - } - if ctxUser := authz.UserFromContext(ctx); ctxUser != nil { - request.UserID = &ctxUser.ID - } - scriptResult, err := svc.ds.NewInternalScriptExecutionRequest(ctx, &request) - if err != nil { - return ctxerr.Wrap(ctx, err, "create script execution request") - } - - // Update the host software installs table with the uninstall request. // Pending uninstalls will automatically show up in the UI Host Details -> Activity -> Upcoming tab. - if err = svc.insertSoftwareUninstallRequest(ctx, scriptResult.ExecutionID, host, installer); err != nil { + execID := uuid.NewString() + if err = svc.insertSoftwareUninstallRequest(ctx, execID, host, installer); err != nil { return err } - return nil } @@ -1818,7 +1806,9 @@ func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *f } } - _, err = svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, installer.InstallerID, true, nil) + _, err = svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, installer.InstallerID, fleet.HostSoftwareInstallOptions{ + SelfService: true, + }) return ctxerr.Wrap(ctx, err, "inserting self-service software install request") } @@ -1852,7 +1842,9 @@ func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *f platform := host.FleetPlatform() mobileAppleDevice := fleet.AppleDevicePlatform(platform) == fleet.IOSPlatform || fleet.AppleDevicePlatform(platform) == fleet.IPadOSPlatform - _, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, true) + _, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, fleet.HostSoftwareInstallOptions{ + SelfService: true, + }) return err } diff --git a/ee/server/service/software_installers_test.go b/ee/server/service/software_installers_test.go index 769d91b7aed9..db97d917c8ab 100644 --- a/ee/server/service/software_installers_test.go +++ b/ee/server/service/software_installers_test.go @@ -105,7 +105,7 @@ func TestInstallUninstallAuth(t *testing.T) { 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, policyID *uint) (string, + ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID uint, softwareInstallerID uint, opts fleet.HostSoftwareInstallOptions) (string, error, ) { return "request_id", nil @@ -113,12 +113,6 @@ func TestInstallUninstallAuth(t *testing.T) { ds.GetAnyScriptContentsFunc = func(ctx context.Context, id uint) ([]byte, error) { return []byte("script"), nil } - ds.NewInternalScriptExecutionRequestFunc = 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 } diff --git a/frontend/__mocks__/activityMock.ts b/frontend/__mocks__/activityMock.ts index cdf84c1b8b79..04458588bda3 100644 --- a/frontend/__mocks__/activityMock.ts +++ b/frontend/__mocks__/activityMock.ts @@ -7,6 +7,7 @@ const DEFAULT_ACTIVITY_MOCK: IActivity = { actor_id: 1, actor_gravatar: "", actor_email: "test@example.com", + fleet_initiated: false, type: ActivityType.EditedAgentOptions, }; diff --git a/frontend/components/ActivityItem/ActivityItem.tsx b/frontend/components/ActivityItem/ActivityItem.tsx index 01451e703ea8..6ab08be8aba9 100644 --- a/frontend/components/ActivityItem/ActivityItem.tsx +++ b/frontend/components/ActivityItem/ActivityItem.tsx @@ -2,7 +2,7 @@ import React from "react"; import ReactTooltip from "react-tooltip"; import classnames from "classnames"; -import { ActivityType, IActivity, IActivityDetails } from "interfaces/activity"; +import { IActivity, IActivityDetails } from "interfaces/activity"; import { addGravatarUrlToResource, internationalTimeFormat, @@ -108,20 +108,6 @@ const ActivityItem = ({ onCancel(); }; - // TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in - // the backend. For now, if all these fields are empty, then we assume it was - // Fleet-initiated. - let fleetInitiated = false; - if ( - !activity.actor_email && - !activity.actor_full_name && - (activity.type === ActivityType.InstalledSoftware || - activity.type === ActivityType.InstalledAppStoreApp || - activity.type === ActivityType.RanScript) - ) { - fleetInitiated = true; - } - return (