mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 14:58:33 +00:00
308 lines
11 KiB
Go
308 lines
11 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"io"
|
||
"net/http"
|
||
"path/filepath"
|
||
|
||
"github.com/fleetdm/fleet/v4/server/authz"
|
||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||
"github.com/go-kit/log/level"
|
||
)
|
||
|
||
func (svc *Service) SetSetupExperienceSoftware(ctx context.Context, platform string, teamID uint, titleIDs []uint) error {
|
||
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
var teamName string
|
||
if teamID == 0 {
|
||
teamName = ""
|
||
ac, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "getting app config")
|
||
}
|
||
if ac.MDM.MacOSSetup.ManualAgentInstall.Value && len(titleIDs) != 0 {
|
||
return fleet.NewUserMessageError(errors.New("Couldn’t add setup experience software. To add software, first disable manual_agent_install."), http.StatusUnprocessableEntity)
|
||
}
|
||
} else {
|
||
team, err := svc.ds.Team(ctx, teamID)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "load team")
|
||
}
|
||
teamName = team.Name
|
||
if team.Config.MDM.MacOSSetup.ManualAgentInstall.Value && len(titleIDs) != 0 {
|
||
return fleet.NewUserMessageError(errors.New("Couldn’t add setup experience software. To add software, first disable manual_agent_install."), http.StatusUnprocessableEntity)
|
||
}
|
||
}
|
||
|
||
if err := svc.ds.SetSetupExperienceSoftwareTitles(ctx, platform, teamID, titleIDs); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "setting setup experience titles")
|
||
}
|
||
|
||
if err := svc.NewActivity(
|
||
ctx,
|
||
authz.UserFromContext(ctx),
|
||
fleet.ActivityEditedSetupExperienceSoftware{
|
||
Platform: platform,
|
||
TeamID: teamID,
|
||
TeamName: teamName,
|
||
},
|
||
); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "create activity for set setup experience software")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) ListSetupExperienceSoftware(ctx context.Context, platform string, teamID uint, opts fleet.ListOptions) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
|
||
TeamID: &teamID,
|
||
}, fleet.ActionRead); err != nil {
|
||
return nil, 0, nil, err
|
||
}
|
||
|
||
titles, count, meta, err := svc.ds.ListSetupExperienceSoftwareTitles(ctx, platform, teamID, opts)
|
||
if err != nil {
|
||
return nil, 0, nil, ctxerr.Wrap(ctx, err, "retrieving list of software setup experience titles")
|
||
}
|
||
|
||
return titles, count, meta, nil
|
||
}
|
||
|
||
func (svc *Service) GetSetupExperienceScript(ctx context.Context, teamID *uint, withContent bool) (*fleet.Script, []byte, error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.Script{TeamID: teamID}, fleet.ActionRead); err != nil {
|
||
return nil, nil, err
|
||
}
|
||
|
||
script, err := svc.ds.GetSetupExperienceScript(ctx, teamID)
|
||
if err != nil {
|
||
return nil, nil, ctxerr.Wrap(ctx, err, "get setup experience script")
|
||
}
|
||
|
||
var content []byte
|
||
if withContent {
|
||
content, err = svc.ds.GetAnyScriptContents(ctx, script.ScriptContentID)
|
||
if err != nil {
|
||
return nil, nil, ctxerr.Wrap(ctx, err, "get setup experience script contents")
|
||
}
|
||
}
|
||
|
||
return script, content, nil
|
||
}
|
||
|
||
func (svc *Service) SetSetupExperienceScript(ctx context.Context, teamID *uint, name string, r io.Reader) error {
|
||
if err := svc.authz.Authorize(ctx, &fleet.Script{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
if teamID == nil {
|
||
ac, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "getting app config")
|
||
}
|
||
if ac.MDM.MacOSSetup.ManualAgentInstall.Value {
|
||
return fleet.NewUserMessageError(errors.New("Couldn’t add setup experience script. To add script, first disable manual_agent_install."), http.StatusUnprocessableEntity)
|
||
}
|
||
} else {
|
||
team, err := svc.ds.Team(ctx, *teamID)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "load team")
|
||
}
|
||
if team.Config.MDM.MacOSSetup.ManualAgentInstall.Value {
|
||
return fleet.NewUserMessageError(errors.New("Couldn’t add setup experience script. To add script, first disable manual_agent_install."), http.StatusUnprocessableEntity)
|
||
}
|
||
}
|
||
|
||
b, err := io.ReadAll(r)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "read setup experience script contents")
|
||
}
|
||
|
||
script := &fleet.Script{
|
||
TeamID: teamID,
|
||
Name: name,
|
||
ScriptContents: string(b),
|
||
}
|
||
|
||
if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{script.ScriptContents}); err != nil {
|
||
return fleet.NewInvalidArgumentError("script", err.Error())
|
||
}
|
||
|
||
// setup experience is only supported for macOS currently so we need to override the file
|
||
// extension check in the general script validation
|
||
if filepath.Ext(script.Name) != ".sh" {
|
||
return fleet.NewInvalidArgumentError("script", "File type not supported. Only .sh file type is allowed.")
|
||
}
|
||
// now we can do our normal script validation
|
||
if err := script.ValidateNewScript(); err != nil {
|
||
return fleet.NewInvalidArgumentError("script", err.Error())
|
||
}
|
||
|
||
if err := svc.ds.SetSetupExperienceScript(ctx, script); err != nil {
|
||
var (
|
||
existsErr fleet.AlreadyExistsError
|
||
fkErr fleet.ForeignKeyError
|
||
)
|
||
if errors.As(err, &existsErr) {
|
||
err = fleet.NewInvalidArgumentError("script", err.Error()).WithStatus(http.StatusConflict) // TODO: confirm error message with product/frontend
|
||
} else if errors.As(err, &fkErr) {
|
||
err = fleet.NewInvalidArgumentError("team_id", "The team does not exist.").WithStatus(http.StatusNotFound)
|
||
}
|
||
return ctxerr.Wrap(ctx, err, "create setup experience script")
|
||
}
|
||
|
||
// NOTE: there is no activity specified for set setup experience script
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) DeleteSetupExperienceScript(ctx context.Context, teamID *uint) error {
|
||
if err := svc.authz.Authorize(ctx, &fleet.Script{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := svc.ds.DeleteSetupExperienceScript(ctx, teamID); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "delete setup experience script")
|
||
}
|
||
|
||
// NOTE: there is no activity specified for delete setup experience script
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) SetupExperienceNextStep(ctx context.Context, host *fleet.Host) (bool, error) {
|
||
hostUUID, err := fleet.HostUUIDForSetupExperience(host)
|
||
if err != nil {
|
||
return false, ctxerr.Wrap(ctx, err, "failed to get host's UUID for the setup experience")
|
||
}
|
||
statuses, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID)
|
||
if err != nil {
|
||
return false, ctxerr.Wrap(ctx, err, "retrieving setup experience status results for next step")
|
||
}
|
||
|
||
var installersPending, appsPending, scriptsPending []*fleet.SetupExperienceStatusResult
|
||
var installersRunning, appsRunning, scriptsRunning int
|
||
|
||
for _, status := range statuses {
|
||
if err := status.IsValid(); err != nil {
|
||
return false, ctxerr.Wrap(ctx, err, "invalid row")
|
||
}
|
||
|
||
switch {
|
||
case status.SoftwareInstallerID != nil:
|
||
switch status.Status {
|
||
case fleet.SetupExperienceStatusPending:
|
||
installersPending = append(installersPending, status)
|
||
case fleet.SetupExperienceStatusRunning:
|
||
installersRunning++
|
||
}
|
||
case status.VPPAppTeamID != nil:
|
||
switch status.Status {
|
||
case fleet.SetupExperienceStatusPending:
|
||
appsPending = append(appsPending, status)
|
||
case fleet.SetupExperienceStatusRunning:
|
||
appsRunning++
|
||
}
|
||
case status.SetupExperienceScriptID != nil:
|
||
switch status.Status {
|
||
case fleet.SetupExperienceStatusPending:
|
||
scriptsPending = append(scriptsPending, status)
|
||
case fleet.SetupExperienceStatusRunning:
|
||
scriptsRunning++
|
||
}
|
||
}
|
||
}
|
||
|
||
switch {
|
||
case len(installersPending) > 0:
|
||
// enqueue installers
|
||
for _, installer := range installersPending {
|
||
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")
|
||
}
|
||
installer.HostSoftwareInstallsExecutionID = &installUUID
|
||
installer.Status = fleet.SetupExperienceStatusRunning
|
||
if err := svc.ds.UpdateSetupExperienceStatusResult(ctx, installer); err != nil {
|
||
return false, ctxerr.Wrap(ctx, err, "updating setup experience result with install uuid")
|
||
}
|
||
}
|
||
case installersRunning == 0 && len(appsPending) > 0:
|
||
// enqueue vpp apps
|
||
for _, app := range appsPending {
|
||
vppAppID, err := app.VPPAppID()
|
||
if err != nil {
|
||
return false, ctxerr.Wrap(ctx, err, "constructing vpp app details for installation")
|
||
}
|
||
|
||
if app.SoftwareTitleID == nil {
|
||
return false, ctxerr.Errorf(ctx, "setup experience software title id missing from vpp app install request: %d", app.ID)
|
||
}
|
||
|
||
vppApp := &fleet.VPPApp{
|
||
TitleID: *app.SoftwareTitleID,
|
||
VPPAppTeam: fleet.VPPAppTeam{
|
||
VPPAppID: *vppAppID,
|
||
},
|
||
}
|
||
|
||
cmdUUID, err := svc.installSoftwareFromVPP(ctx, host, vppApp, true, fleet.HostSoftwareInstallOptions{
|
||
SelfService: false,
|
||
ForSetupExperience: true,
|
||
})
|
||
|
||
app.NanoCommandUUID = &cmdUUID
|
||
app.Status = fleet.SetupExperienceStatusRunning
|
||
|
||
if err != nil {
|
||
// if we get an error (e.g. no available licenses) while attempting to enqueue the
|
||
// install, then we should immediately go to an error state so setup experience
|
||
// isn't blocked.
|
||
level.Warn(svc.logger).Log("msg", "got an error when attempting to enqueue VPP app install", "err", err, "adam_id", app.VPPAppAdamID)
|
||
app.Status = fleet.SetupExperienceStatusFailure
|
||
app.Error = ptr.String(err.Error())
|
||
}
|
||
if err := svc.ds.UpdateSetupExperienceStatusResult(ctx, app); err != nil {
|
||
return false, ctxerr.Wrap(ctx, err, "updating setup experience with vpp install command uuid")
|
||
}
|
||
}
|
||
case installersRunning == 0 && appsRunning == 0 && len(scriptsPending) > 0:
|
||
// enqueue scripts
|
||
for _, script := range scriptsPending {
|
||
if script.ScriptContentID == nil {
|
||
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,
|
||
// 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)
|
||
if err != nil {
|
||
return false, ctxerr.Wrap(ctx, err, "queueing setup experience script execution request")
|
||
}
|
||
script.ScriptExecutionID = &res.ExecutionID
|
||
script.Status = fleet.SetupExperienceStatusRunning
|
||
if err := svc.ds.UpdateSetupExperienceStatusResult(ctx, script); err != nil {
|
||
return false, ctxerr.Wrap(ctx, err, "updating setup experience script execution id")
|
||
}
|
||
}
|
||
case installersRunning == 0 && appsRunning == 0 && scriptsRunning == 0:
|
||
// finished
|
||
return true, nil
|
||
}
|
||
|
||
return false, nil
|
||
}
|