Files
fleet/ee/server/service/orbit.go
T
Gabriel Hernandez 1c591e637c add pagination metadata to get mdm commands endpoint (#37396)
**Related issue:** Resolves #37335

This adds pagination metadata to the `GET /mdm/commands` endpoint. 

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [ ] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
2025-12-18 11:53:27 +00:00

403 lines
14 KiB
Go

package service
import (
"context"
"strings"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/go-kit/log/level"
"github.com/google/uuid"
)
func (svc *Service) GetOrbitSetupExperienceStatus(ctx context.Context, orbitNodeKey string, forceRelease bool, resetFailedSetupSteps bool) (*fleet.SetupExperienceStatusPayload, error) {
// this is not a user-authenticated endpoint
svc.authz.SkipAuthorization(ctx)
host, ok := hostctx.FromContext(ctx)
if !ok {
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
return nil, err
}
if fleet.IsLinux(host.Platform) || host.Platform == "windows" {
// Windows and Linux setup experience only have software.
status, err := svc.getHostSetupExperienceStatus(ctx, host)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get host setup experience status")
}
return &fleet.SetupExperienceStatusPayload{
Software: status.Software,
}, nil
}
appCfg, err := svc.ds.AppConfig(ctx)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting app config")
}
// get the status of the bootstrap package deployment
bootstrapPkg, err := svc.ds.GetHostMDMMacOSSetup(ctx, host.ID)
if err != nil && !fleet.IsNotFound(err) {
return nil, ctxerr.Wrap(ctx, err, "get bootstrap package status")
}
// NOTE: bootstrapPkg can be nil if there was none to install.
var bootstrapPkgResult *fleet.SetupExperienceBootstrapPackageResult
if bootstrapPkg != nil {
bootstrapPkgResult = &fleet.SetupExperienceBootstrapPackageResult{
Name: bootstrapPkg.BootstrapPackageName,
Status: bootstrapPkg.BootstrapPackageStatus,
}
}
// get the status of the configuration profiles
cfgProfs, err := svc.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get configuration profiles status")
}
var cfgProfResults []*fleet.SetupExperienceConfigurationProfileResult
for _, prof := range cfgProfs {
// NOTE: DDM profiles (declarations) are ignored because while a device is
// awaiting to be released, it cannot process a DDM session (at least
// that's what we noticed during testing).
if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) {
continue
}
// NOTE: user-scoped profiles are ignored because they are not sent by Fleet
// until after the device is released - there is no user-channel available
// on the host until after the release, and after the user actually created
// the user account.
if prof.Scope == fleet.PayloadScopeUser {
continue
}
status := fleet.MDMDeliveryPending
if prof.Status != nil {
status = *prof.Status
}
cfgProfResults = append(cfgProfResults, &fleet.SetupExperienceConfigurationProfileResult{
ProfileUUID: prof.ProfileUUID,
Name: prof.Name,
Status: status,
})
}
profilesMissingInstallation, err := svc.ds.ListMDMAppleProfilesToInstall(ctx, host.UUID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing apple config profiles to install")
}
profilesMissingInstallation = fleet.FilterOutUserScopedProfiles(profilesMissingInstallation)
if host.Platform != "darwin" {
profilesMissingInstallation = fleet.FilterMacOSOnlyProfilesFromIOSIPadOS(profilesMissingInstallation)
}
if len(profilesMissingInstallation) > 0 {
for _, prof := range profilesMissingInstallation {
cfgProfResults = append(cfgProfResults, &fleet.SetupExperienceConfigurationProfileResult{
ProfileUUID: prof.ProfileUUID,
Name: prof.ProfileName,
Status: fleet.MDMDeliveryPending, // Default to pending as it's not installed yet.
})
}
}
// AccountConfiguration covers the (optional) command to setup SSO.
adminTeamFilter := fleet.TeamFilter{
User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
}
acctCmds, _, _, err := svc.ds.ListMDMCommands(ctx, adminTeamFilter, &fleet.MDMCommandListOptions{
Filters: fleet.MDMCommandFilters{
HostIdentifier: host.UUID,
RequestType: "AccountConfiguration",
},
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "list AccountConfiguration commands")
}
var acctCfgResult *fleet.SetupExperienceAccountConfigurationResult
if len(acctCmds) > 0 {
// there may be more than one if e.g. the worker job that sends them had to
// retry, but they would all be processed anyway so we can only care about
// the first one.
acctCfgResult = &fleet.SetupExperienceAccountConfigurationResult{
CommandUUID: acctCmds[0].CommandUUID,
Status: acctCmds[0].Status,
}
}
// get status of software installs and script execution
res, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing setup experience results")
}
// Check if "require all software" is configured for the host's team.
requireAllSoftware, err := svc.IsAllSetupExperienceSoftwareRequired(ctx, host)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "checking if all software is required")
}
hasFailedSoftwareInstall := false
for _, r := range res {
if r.IsForSoftware() && r.Status == fleet.SetupExperienceStatusFailure {
hasFailedSoftwareInstall = true
break
}
}
// If we have a failed software install,
// AND "require all software" is configured for the host's team,
// AND the resetFailedSetupSteps flag is set,
// then re-enqueue any cancelled setup experience steps.
if hasFailedSoftwareInstall {
if resetFailedSetupSteps {
teamID := uint(0)
if host.TeamID != nil {
teamID = *host.TeamID
}
// If so, call the enqueue function with a flag to retain successful steps.
if requireAllSoftware {
level.Info(svc.logger).Log("msg", "re-enqueueing cancelled setup experience steps after a previous software install failure", "host_uuid", host.UUID)
platform := host.PlatformLike
if platform == "" {
platform = host.Platform
}
_, err := svc.ds.ResetSetupExperienceItemsAfterFailure(ctx, platform, host.UUID, teamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "re-enqueueing cancelled setup experience steps after a previous software install failure")
}
// Re-fetch the setup experience results after re-enqueuing.
res, err = svc.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing setup experience results")
}
}
}
}
err = svc.failCancelledSetupExperienceInstalls(ctx, host.ID, host.UUID, host.DisplayName(), res)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "failing cancelled setup experience installs")
}
payload := &fleet.SetupExperienceStatusPayload{
BootstrapPackage: bootstrapPkgResult,
ConfigurationProfiles: cfgProfResults,
AccountConfiguration: acctCfgResult,
Software: make([]*fleet.SetupExperienceStatusResult, 0),
OrgLogoURL: appCfg.OrgInfo.OrgLogoURLLightBackground,
RequireAllSoftware: requireAllSoftware,
}
for _, r := range res {
if r.IsForScript() {
payload.Script = r
}
if r.IsForSoftware() {
payload.Software = append(payload.Software, r)
}
}
// If we have failed software, and all software is required,
// we can go ahead and return now.
if hasFailedSoftwareInstall && requireAllSoftware {
return payload, nil
}
if forceRelease || isDeviceReadyForRelease(payload) {
manual, err := isDeviceReleasedManually(ctx, svc.ds, host)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "check if device is released manually")
}
if manual {
return payload, nil
}
// otherwise the device is not released manually, proceed with automatic
// release
if forceRelease {
level.Warn(svc.logger).Log("msg", "force-releasing device, DEP enrollment commands, profiles, software installs and script execution may not have all completed", "host_uuid", host.UUID)
} else {
level.Info(svc.logger).Log("msg", "releasing device, all DEP enrollment commands, profiles, software installs and script execution have completed", "host_uuid", host.UUID)
}
// Host will be marked as no longer "awaiting configuration" in the command handler
if err := svc.mdmAppleCommander.DeviceConfigured(ctx, host.UUID, uuid.NewString()); err != nil {
return nil, ctxerr.Wrap(ctx, err, "failed to enqueue DeviceConfigured command")
}
}
_, err = svc.SetupExperienceNextStep(ctx, host)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting next step for host setup experience")
}
return payload, nil
}
func (svc *Service) failCancelledSetupExperienceInstalls(
ctx context.Context,
hostID uint,
hostUUID string,
hostDisplayName string,
results []*fleet.SetupExperienceStatusResult,
) error {
for _, r := range results {
if r.Status != fleet.SetupExperienceStatusCancelled {
continue
}
r.Status = fleet.SetupExperienceStatusFailure
level.Info(svc.logger).Log("msg", "marking setup experience software as failed due to cancellation", "host_uuid", hostUUID, "software_name", r.Name)
err := svc.ds.UpdateSetupExperienceStatusResult(ctx, r)
if err != nil {
return ctxerr.Wrap(ctx, err, "failing cancelled setup experience software install")
}
// TODO -- support recording activity for failed VPP apps as well.
// https://github.com/fleetdm/fleet/issues/34288
if r.IsForSoftwarePackage() {
softwarePackage := ""
var source *string
installerMeta, err := svc.ds.GetSoftwareInstallerMetadataByID(ctx, *r.SoftwareInstallerID)
if err != nil && !fleet.IsNotFound(err) {
return ctxerr.Wrap(ctx, err, "getting software installer metadata for cancelled setup experience software install")
}
if installerMeta != nil {
softwarePackage = installerMeta.Name
// Get the software title to retrieve the source
if installerMeta.TitleID != nil {
title, err := svc.ds.SoftwareTitleByID(ctx, *installerMeta.TitleID, nil, fleet.TeamFilter{})
if err != nil && !fleet.IsNotFound(err) {
return ctxerr.Wrap(ctx, err, "getting software title for cancelled setup experience software install")
}
if title != nil {
source = &title.Source
}
}
}
activity := fleet.ActivityTypeInstalledSoftware{
HostID: hostID,
HostDisplayName: hostDisplayName,
SoftwareTitle: r.Name,
SoftwarePackage: softwarePackage,
InstallUUID: *r.HostSoftwareInstallsExecutionID,
Status: "failed",
SelfService: false,
Source: source,
FromSetupExperience: true,
}
err = svc.NewActivity(ctx, nil, activity)
if err != nil {
return ctxerr.Wrap(ctx, err, "creating activity for cancelled setup experience software install")
}
}
continue
}
return nil
}
func isDeviceReleasedManually(ctx context.Context, ds fleet.Datastore, host *fleet.Host) (bool, error) {
var manualRelease bool
if host.TeamID == nil {
ac, err := ds.AppConfig(ctx)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "get AppConfig to read enable_release_device_manually")
}
manualRelease = ac.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
} else {
tm, err := ds.TeamLite(ctx, *host.TeamID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "get Team to read enable_release_device_manually")
}
manualRelease = tm.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
}
return manualRelease, nil
}
func isDeviceReadyForRelease(payload *fleet.SetupExperienceStatusPayload) bool {
// default to "do release" and return false as soon as we find a reason not
// to.
if payload.BootstrapPackage != nil {
if payload.BootstrapPackage.Status != fleet.MDMBootstrapPackageFailed &&
payload.BootstrapPackage.Status != fleet.MDMBootstrapPackageInstalled {
// bootstrap package is still pending, not ready for release
return false
}
}
if payload.AccountConfiguration != nil {
if payload.AccountConfiguration.Status != fleet.MDMAppleStatusAcknowledged &&
payload.AccountConfiguration.Status != fleet.MDMAppleStatusError &&
payload.AccountConfiguration.Status != fleet.MDMAppleStatusCommandFormatError {
// account configuration command is still pending, not ready for release
return false
}
}
for _, prof := range payload.ConfigurationProfiles {
if prof.Status != fleet.MDMDeliveryFailed &&
prof.Status != fleet.MDMDeliveryVerifying &&
prof.Status != fleet.MDMDeliveryVerified {
// profile is still pending, not ready for release
return false
}
}
for _, sw := range payload.Software {
if sw.Status != fleet.SetupExperienceStatusFailure &&
sw.Status != fleet.SetupExperienceStatusSuccess {
// software is still pending, not ready for release
return false
}
}
if payload.Script != nil {
if payload.Script.Status != fleet.SetupExperienceStatusFailure &&
payload.Script.Status != fleet.SetupExperienceStatusSuccess {
// script is still pending, not ready for release
return false
}
}
return true
}
func (svc *Service) SetupExperienceInit(ctx context.Context) (*fleet.SetupExperienceInitResult, error) {
// This is an orbit endpoint, not a user-authenticated endpoint.
svc.authz.SkipAuthorization(ctx)
// NOTE: currently, Android does not go through the "init" setup experience flow as it
// doesn't support any on-device UI (such as the screen showing setup progress) nor any
// ordering of installs - all software to install is provided as part of the Android policy
// when the host enrolls in Fleet.
// See https://github.com/fleetdm/fleet/issues/33761#issuecomment-3548996114
host, ok := hostctx.FromContext(ctx)
if !ok {
return nil, ctxerr.New(ctx, "internal error: missing host from request context")
}
// teamID for EnqueueSetupExperienceItems should be 0 for "No team" hosts.
var teamID uint
if host.TeamID != nil {
teamID = *host.TeamID
}
hostUUID, err := fleet.HostUUIDForSetupExperience(host)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "failed to get host's UUID for the setup experience")
}
enabled, err := svc.ds.EnqueueSetupExperienceItems(ctx, host.PlatformLike, hostUUID, teamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "check for software titles for setup experience")
}
return &fleet.SetupExperienceInitResult{
Enabled: enabled,
}, nil
}