mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
757 lines
27 KiB
Go
757 lines
27 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/authz"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func (svc *Service) GetHost(ctx context.Context, id uint, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) {
|
|
// reuse GetHost, but include premium details
|
|
opts.IncludeCVEScores = true
|
|
opts.IncludePolicies = true
|
|
opts.IncludeCriticalVulnerabilitiesCount = true
|
|
return svc.Service.GetHost(ctx, id, opts)
|
|
}
|
|
|
|
func (svc *Service) HostByIdentifier(ctx context.Context, identifier string, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) {
|
|
// reuse HostByIdentifier, but include premium options
|
|
opts.IncludeCVEScores = true
|
|
opts.IncludePolicies = true
|
|
return svc.Service.HostByIdentifier(ctx, identifier, opts)
|
|
}
|
|
|
|
func (svc *Service) OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string, opts fleet.ListOptions, _ bool,
|
|
maxVulnerabilities *int) (*fleet.OSVersions, int, *fleet.PaginationMetadata, error) {
|
|
// reuse OSVersions, but include premium options
|
|
return svc.Service.OSVersions(ctx, teamID, platform, name, version, opts, true, maxVulnerabilities)
|
|
}
|
|
|
|
func (svc *Service) OSVersion(ctx context.Context, osID uint, teamID *uint, _ bool, maxVulnerabilities *int) (*fleet.OSVersion, *time.Time, error) {
|
|
// reuse OSVersion, but include premium options
|
|
return svc.Service.OSVersion(ctx, osID, teamID, true, maxVulnerabilities)
|
|
}
|
|
|
|
func (svc *Service) LockHost(ctx context.Context, hostID uint, viewPIN bool) (unlockPIN string, err error) {
|
|
// First ensure the user has access to list hosts, then check the specific
|
|
// host once team_id is loaded.
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
return "", err
|
|
}
|
|
host, err := svc.ds.Host(ctx, hostID)
|
|
if err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "get host lite")
|
|
}
|
|
|
|
// Authorize again with team loaded now that we have the host's team_id.
|
|
// Authorize as "execute mdm_command", which is the correct access
|
|
// requirement and is what happens for macOS platforms.
|
|
if err := svc.authz.Authorize(ctx, fleet.MDMCommandAuthz{TeamID: host.TeamID}, fleet.ActionWrite); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// locking validations are based on the platform of the host
|
|
switch host.FleetPlatform() {
|
|
case "darwin", "ios", "ipados":
|
|
if host.MDM.EnrollmentStatus != nil && *host.MDM.EnrollmentStatus == "On (personal)" {
|
|
return "", &fleet.BadRequestError{
|
|
Message: fleet.CantLockPersonalHostsMessage,
|
|
}
|
|
}
|
|
if host.MDM.EnrollmentStatus != nil && *host.MDM.EnrollmentStatus == "On (manual)" &&
|
|
(host.FleetPlatform() == "ios" || host.FleetPlatform() == "ipados") {
|
|
return "", &fleet.BadRequestError{
|
|
Message: fleet.CantLockManualIOSIpadOSHostsMessage,
|
|
}
|
|
}
|
|
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
|
|
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
|
err = fleet.NewInvalidArgumentError("host_id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
|
}
|
|
return "", ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
|
|
}
|
|
|
|
// on macOS, the lock command requires the host to be MDM-enrolled in Fleet
|
|
connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host)
|
|
if err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet")
|
|
}
|
|
if !connected {
|
|
return "", ctxerr.Wrap(
|
|
ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."),
|
|
)
|
|
}
|
|
|
|
case "windows", "linux":
|
|
if host.FleetPlatform() == "windows" {
|
|
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
|
|
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
|
err = fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
|
}
|
|
return "", ctxerr.Wrap(ctx, err, "check windows MDM enabled")
|
|
}
|
|
}
|
|
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
|
|
switch {
|
|
case err != nil:
|
|
// If not found, then do nothing. We do not know if this host has scripts enabled or not
|
|
if !fleet.IsNotFound(err) {
|
|
return "", ctxerr.Wrap(ctx, err, "get host orbit info")
|
|
}
|
|
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
|
|
return "", ctxerr.Wrap(
|
|
ctx, fleet.NewInvalidArgumentError(
|
|
"host_id", "Couldn't lock host. To lock, deploy the fleetd agent with --enable-scripts and refetch host vitals.",
|
|
),
|
|
)
|
|
}
|
|
|
|
default:
|
|
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
|
}
|
|
|
|
// if there's a lock, unlock or wipe action pending, do not accept the lock
|
|
// request.
|
|
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host)
|
|
if err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
|
}
|
|
|
|
switch {
|
|
case lockWipe.IsPendingLock():
|
|
return "", ctxerr.Wrap(
|
|
ctx, fleet.NewInvalidArgumentError(
|
|
"host_id", "Host has pending lock request. Host cannot be locked again until lock is complete.",
|
|
),
|
|
)
|
|
case lockWipe.IsPendingUnlock():
|
|
return "", ctxerr.Wrap(
|
|
ctx, fleet.NewInvalidArgumentError(
|
|
"host_id", "Host has pending unlock request. Host cannot be locked again until unlock is complete.",
|
|
),
|
|
)
|
|
case lockWipe.IsPendingWipe():
|
|
return "", ctxerr.Wrap(
|
|
ctx,
|
|
fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process lock requests once host is wiped."),
|
|
)
|
|
case lockWipe.IsWiped():
|
|
return "", ctxerr.Wrap(
|
|
ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process lock requests once host is wiped."),
|
|
)
|
|
case lockWipe.IsLocked():
|
|
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already locked.").WithStatus(http.StatusConflict))
|
|
}
|
|
|
|
// all good, go ahead with queuing the lock request.
|
|
return svc.enqueueLockHostRequest(ctx, host, lockWipe, viewPIN)
|
|
}
|
|
|
|
func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error) {
|
|
// First ensure the user has access to list hosts, then check the specific
|
|
// host once team_id is loaded.
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
return "", err
|
|
}
|
|
host, err := svc.ds.HostLite(ctx, hostID)
|
|
if err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "get host lite")
|
|
}
|
|
|
|
// Authorize again with team loaded now that we have the host's team_id.
|
|
// Authorize as "execute mdm_command", which is the correct access
|
|
// requirement.
|
|
if err := svc.authz.Authorize(ctx, fleet.MDMCommandAuthz{TeamID: host.TeamID}, fleet.ActionWrite); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// locking validations are based on the platform of the host
|
|
switch host.FleetPlatform() {
|
|
case "darwin", "ios", "ipados":
|
|
// all good, no need to check if MDM enrolled, will validate later that it
|
|
// is currently locked.
|
|
|
|
case "windows", "linux":
|
|
// on Windows and Linux, a script is used to unlock the host so scripts must
|
|
// be enabled on the host
|
|
if host.FleetPlatform() == "windows" {
|
|
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
|
|
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
|
err = fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
|
}
|
|
return "", ctxerr.Wrap(ctx, err, "check windows MDM enabled")
|
|
}
|
|
}
|
|
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
|
|
switch {
|
|
case err != nil:
|
|
// If not found, then do nothing. We do not know if this host has scripts enabled or not
|
|
if !fleet.IsNotFound(err) {
|
|
return "", ctxerr.Wrap(ctx, err, "get host orbit info")
|
|
}
|
|
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
|
|
return "", ctxerr.Wrap(
|
|
ctx, fleet.NewInvalidArgumentError(
|
|
"host_id", "Couldn't unlock host. To unlock, deploy the fleetd agent with --enable-scripts and refetch host vitals.",
|
|
),
|
|
)
|
|
}
|
|
|
|
default:
|
|
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
|
}
|
|
|
|
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host)
|
|
if err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
|
}
|
|
switch {
|
|
case lockWipe.IsPendingLock():
|
|
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending lock request. Host cannot be unlocked until lock is complete."))
|
|
case lockWipe.IsPendingUnlock():
|
|
// MacOS machines are unlocked by typing the PIN into the machine. "Unlock" in this case
|
|
// should just return the PIN as many times as needed.
|
|
// Breaking here will fall through to call enqueueUnLockHostRequest which will return the PIN.
|
|
if host.FleetPlatform() == "darwin" {
|
|
break
|
|
}
|
|
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. The host will unlock when it comes online."))
|
|
case lockWipe.IsPendingWipe():
|
|
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process unlock requests once host is wiped."))
|
|
case lockWipe.IsWiped():
|
|
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process unlock requests once host is wiped."))
|
|
case lockWipe.IsUnlocked():
|
|
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already unlocked.").WithStatus(http.StatusConflict))
|
|
}
|
|
|
|
// all good, go ahead with queuing the unlock request.
|
|
return svc.enqueueUnlockHostRequest(ctx, host, lockWipe)
|
|
}
|
|
|
|
func (svc *Service) WipeHost(ctx context.Context, hostID uint, metadata *fleet.MDMWipeMetadata) error {
|
|
// First ensure the user has access to list hosts, then check the specific
|
|
// host once team_id is loaded.
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
return err
|
|
}
|
|
host, err := svc.ds.Host(ctx, hostID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get host lite")
|
|
}
|
|
|
|
// Authorize again with team loaded now that we have the host's team_id.
|
|
// Authorize as "execute mdm_command", which is the correct access
|
|
// requirement and is what happens for macOS platforms.
|
|
if err := svc.authz.Authorize(ctx, fleet.MDMCommandAuthz{TeamID: host.TeamID}, fleet.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
// wipe validations are based on the platform of the host, Windows and macOS
|
|
// require MDM to be enabled and the host to be MDM-enrolled in Fleet. Linux
|
|
// uses scripts, not MDM.
|
|
var requireMDM bool
|
|
switch host.FleetPlatform() {
|
|
case "darwin", "ios", "ipados":
|
|
if host.MDM.EnrollmentStatus != nil && *host.MDM.EnrollmentStatus == "On (personal)" {
|
|
return &fleet.BadRequestError{
|
|
Message: fleet.CantWipePersonalHostsMessage,
|
|
}
|
|
}
|
|
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
|
|
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
|
err = fleet.NewInvalidArgumentError("host_id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
|
}
|
|
return ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
|
|
}
|
|
requireMDM = true
|
|
|
|
case "windows":
|
|
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
|
|
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
|
err = fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
|
}
|
|
return ctxerr.Wrap(ctx, err, "check windows MDM enabled")
|
|
}
|
|
requireMDM = true
|
|
|
|
case "linux":
|
|
// on linux, a script is used to wipe the host so scripts must be enabled on the host
|
|
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
|
|
switch {
|
|
case err != nil:
|
|
// If not found, then do nothing. We do not know if this host has scripts enabled or not
|
|
if !fleet.IsNotFound(err) {
|
|
return ctxerr.Wrap(ctx, err, "get host orbit info")
|
|
}
|
|
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
|
|
return ctxerr.Wrap(
|
|
ctx, fleet.NewInvalidArgumentError(
|
|
"host_id", "Couldn't wipe host. To wipe, deploy the fleetd agent with --enable-scripts and refetch host vitals.",
|
|
),
|
|
)
|
|
}
|
|
|
|
default:
|
|
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
|
}
|
|
|
|
if requireMDM {
|
|
// the wipe command requires the host to be MDM-enrolled in Fleet
|
|
connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet")
|
|
}
|
|
if !connected {
|
|
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't wipe the host because it doesn't have MDM turned on."))
|
|
}
|
|
}
|
|
|
|
// validations based on host's actions status (pending lock, unlock, wipe)
|
|
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
|
}
|
|
switch {
|
|
case lockWipe.IsPendingLock():
|
|
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending lock request. Host cannot be wiped until lock is complete."))
|
|
case lockWipe.IsPendingUnlock():
|
|
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. Host cannot be wiped until unlock is complete."))
|
|
case lockWipe.IsPendingWipe():
|
|
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. The host will be wiped when it comes online."))
|
|
case lockWipe.IsLocked():
|
|
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is locked. Host cannot be wiped until it is unlocked."))
|
|
case lockWipe.IsWiped():
|
|
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already wiped.").WithStatus(http.StatusConflict))
|
|
}
|
|
|
|
// all good, go ahead with queuing the wipe request.
|
|
return svc.enqueueWipeHostRequest(ctx, host, lockWipe, metadata)
|
|
}
|
|
|
|
func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus, viewPIN bool) (
|
|
unlockPIN string, err error,
|
|
) {
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return "", fleet.ErrNoContext
|
|
}
|
|
|
|
activity := fleet.ActivityTypeLockedHost{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
}
|
|
|
|
switch lockStatus.HostFleetPlatform {
|
|
case "darwin":
|
|
lockCommandUUID := uuid.NewString()
|
|
if unlockPIN, err = svc.mdmAppleCommander.DeviceLock(ctx, host, lockCommandUUID); err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "enqueuing lock request for macOS")
|
|
}
|
|
activity.ViewPIN = viewPIN
|
|
case "ios", "ipados":
|
|
appCfg, err := svc.ds.AppConfig(ctx)
|
|
if err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "get app config")
|
|
}
|
|
lockCommandUUID := uuid.NewString()
|
|
if err := svc.mdmAppleCommander.EnableLostMode(ctx, host, lockCommandUUID, appCfg.OrgInfo.OrgName); err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "enabling lost mode for iOS/iPadOS")
|
|
}
|
|
case "windows":
|
|
// TODO(mna): svc.RunHostScript should be refactored so that we can reuse the
|
|
// part starting with the validation of the script (just in case), the checks
|
|
// that we don't enqueue over the limit, etc. for any other important
|
|
// validation we may add over there and that we bypass here by enqueueing the
|
|
// script directly in the datastore layer.
|
|
if err := svc.ds.LockHostViaScript(ctx, &fleet.HostScriptRequestPayload{
|
|
HostID: host.ID,
|
|
ScriptContents: string(windowsLockScript),
|
|
UserID: &vc.User.ID,
|
|
SyncRequest: false,
|
|
}, host.FleetPlatform()); err != nil {
|
|
return "", err
|
|
}
|
|
case "linux":
|
|
// TODO(mna): svc.RunHostScript should be refactored so that we can reuse the
|
|
// part starting with the validation of the script (just in case), the checks
|
|
// that we don't enqueue over the limit, etc. for any other important
|
|
// validation we may add over there and that we bypass here by enqueueing the
|
|
// script directly in the datastore layer.
|
|
if err := svc.ds.LockHostViaScript(ctx, &fleet.HostScriptRequestPayload{
|
|
HostID: host.ID,
|
|
ScriptContents: string(linuxLockScript),
|
|
UserID: &vc.User.ID,
|
|
SyncRequest: false,
|
|
}, host.FleetPlatform()); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
vc.User,
|
|
activity,
|
|
); err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "create activity for lock host request")
|
|
}
|
|
|
|
return unlockPIN, nil
|
|
}
|
|
|
|
func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) (unlockPIN string, err error) {
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return "", fleet.ErrNoContext
|
|
}
|
|
|
|
switch lockStatus.HostFleetPlatform {
|
|
case "darwin":
|
|
// Record the unlock request time if it was not already recorded.
|
|
// It should be always recorded, since the UnlockRequestedAt time is created after the lock command is acknowledged.
|
|
// This code is left here to catch potential issues.
|
|
if lockStatus.UnlockRequestedAt.IsZero() {
|
|
if err := svc.ds.UnlockHostManually(ctx, host.ID, host.FleetPlatform(), time.Now().UTC()); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
unlockPIN = lockStatus.UnlockPIN
|
|
case "ios", "ipados":
|
|
err := svc.mdmAppleCommander.DisableLostMode(ctx, host, uuid.NewString())
|
|
if err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "disabling lost mode for iOS/iPadOS")
|
|
}
|
|
case "windows":
|
|
// TODO(mna): svc.RunHostScript should be refactored so that we can reuse the
|
|
// part starting with the validation of the script (just in case), the checks
|
|
// that we don't enqueue over the limit, etc. for any other important
|
|
// validation we may add over there and that we bypass here by enqueueing the
|
|
// script directly in the datastore layer.
|
|
if err := svc.ds.UnlockHostViaScript(ctx, &fleet.HostScriptRequestPayload{
|
|
HostID: host.ID,
|
|
ScriptContents: string(windowsUnlockScript),
|
|
UserID: &vc.User.ID,
|
|
SyncRequest: false,
|
|
}, host.FleetPlatform()); err != nil {
|
|
return "", err
|
|
}
|
|
case "linux":
|
|
// TODO(mna): svc.RunHostScript should be refactored so that we can reuse the
|
|
// part starting with the validation of the script (just in case), the checks
|
|
// that we don't enqueue over the limit, etc. for any other important
|
|
// validation we may add over there and that we bypass here by enqueueing the
|
|
// script directly in the datastore layer.
|
|
if err := svc.ds.UnlockHostViaScript(ctx, &fleet.HostScriptRequestPayload{
|
|
HostID: host.ID,
|
|
ScriptContents: string(linuxUnlockScript),
|
|
UserID: &vc.User.ID,
|
|
SyncRequest: false,
|
|
}, host.FleetPlatform()); err != nil {
|
|
return "", err
|
|
}
|
|
default:
|
|
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
vc.User,
|
|
fleet.ActivityTypeUnlockedHost{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
HostPlatform: host.Platform,
|
|
},
|
|
); err != nil {
|
|
return "", ctxerr.Wrap(ctx, err, "create activity for unlock host request")
|
|
}
|
|
|
|
return unlockPIN, nil
|
|
}
|
|
|
|
func (svc *Service) enqueueWipeHostRequest(
|
|
ctx context.Context,
|
|
host *fleet.Host,
|
|
wipeStatus *fleet.HostLockWipeStatus,
|
|
metadata *fleet.MDMWipeMetadata,
|
|
) error {
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return fleet.ErrNoContext
|
|
}
|
|
|
|
switch wipeStatus.HostFleetPlatform {
|
|
case "darwin", "ios", "ipados":
|
|
wipeCommandUUID := uuid.NewString()
|
|
if err := svc.mdmAppleCommander.EraseDevice(ctx, host, wipeCommandUUID); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "enqueuing wipe request for darwin")
|
|
}
|
|
|
|
case "windows":
|
|
// default wipe type
|
|
wipeType := fleet.MDMWindowsWipeTypeDoWipeProtected
|
|
if metadata != nil && metadata.Windows != nil {
|
|
wipeType = metadata.Windows.WipeType
|
|
svc.logger.DebugContext(ctx, "Windows host wipe request", "wipe_type", wipeType.String())
|
|
}
|
|
wipeCmdUUID := uuid.NewString()
|
|
wipeCmd := &fleet.MDMWindowsCommand{
|
|
CommandUUID: wipeCmdUUID,
|
|
RawCommand: []byte(fmt.Sprintf(windowsWipeCommand, wipeCmdUUID, wipeType.String())),
|
|
TargetLocURI: fmt.Sprintf("./Device/Vendor/MSFT/RemoteWipe/%s", wipeType.String()),
|
|
}
|
|
if err := svc.ds.WipeHostViaWindowsMDM(ctx, host, wipeCmd); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "enqueuing wipe request for windows")
|
|
}
|
|
|
|
case "linux":
|
|
// TODO(mna): svc.RunHostScript should be refactored so that we can reuse the
|
|
// part starting with the validation of the script (just in case), the checks
|
|
// that we don't enqueue over the limit, etc. for any other important
|
|
// validation we may add over there and that we bypass here by enqueueing the
|
|
// script directly in the datastore layer.
|
|
if err := svc.ds.WipeHostViaScript(ctx, &fleet.HostScriptRequestPayload{
|
|
HostID: host.ID,
|
|
ScriptContents: string(linuxWipeScript),
|
|
UserID: &vc.User.ID,
|
|
SyncRequest: false,
|
|
}, host.FleetPlatform()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
vc.User,
|
|
fleet.ActivityTypeWipedHost{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
},
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "create activity for wipe host request")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (svc *Service) RotateRecoveryLockPassword(ctx context.Context, hostID uint) error {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
return err
|
|
}
|
|
host, err := svc.ds.Host(ctx, hostID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get host")
|
|
}
|
|
|
|
// Authorize again with team loaded now that we have the host's team_id.
|
|
// Authorize as "execute mdm_command", which is the correct access requirement.
|
|
if err := svc.authz.Authorize(ctx, fleet.MDMCommandAuthz{TeamID: host.TeamID}, fleet.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate: must be Apple Silicon Mac (macOS with ARM CPU)
|
|
if !host.IsAppleSilicon() {
|
|
return &fleet.BadRequestError{
|
|
Message: "Recovery lock password rotation is only supported on Apple Silicon Macs.",
|
|
}
|
|
}
|
|
|
|
// Validate: must be MDM enrolled in Fleet
|
|
connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet MDM")
|
|
}
|
|
if !connected {
|
|
return &fleet.BadRequestError{
|
|
Message: "Host must be enrolled in Fleet MDM to rotate the recovery lock password.",
|
|
}
|
|
}
|
|
|
|
// Check if recovery lock password is enabled for this team/no-team
|
|
appCfg, err := svc.ds.AppConfig(ctx)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get app config")
|
|
}
|
|
|
|
recoveryLockEnabled := false
|
|
if host.TeamID != nil {
|
|
team, err := svc.ds.TeamLite(ctx, *host.TeamID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get team")
|
|
}
|
|
recoveryLockEnabled = team.Config.MDM.EnableRecoveryLockPassword
|
|
} else {
|
|
recoveryLockEnabled = appCfg.MDM.EnableRecoveryLockPassword.Value
|
|
}
|
|
|
|
if !recoveryLockEnabled {
|
|
return &fleet.BadRequestError{
|
|
Message: "Recovery lock password is not enabled for this host's team.",
|
|
}
|
|
}
|
|
|
|
// Get the current rotation status
|
|
rotationStatus, err := svc.ds.GetRecoveryLockRotationStatus(ctx, host.UUID)
|
|
if err != nil {
|
|
if fleet.IsNotFound(err) {
|
|
return &fleet.BadRequestError{
|
|
Message: "Host does not have a recovery lock password to rotate.",
|
|
}
|
|
}
|
|
return ctxerr.Wrap(ctx, err, "get recovery lock rotation status")
|
|
}
|
|
|
|
// Validate: must have an existing password
|
|
if !rotationStatus.HasPassword {
|
|
return &fleet.BadRequestError{
|
|
Message: "Host does not have a recovery lock password to rotate.",
|
|
}
|
|
}
|
|
|
|
// Validate: not already rotating
|
|
if rotationStatus.HasPendingRotation {
|
|
return &fleet.ConflictError{
|
|
Message: "Recovery lock password rotation is already in progress for this host.",
|
|
}
|
|
}
|
|
|
|
// Validate: must be in install operation (not remove)
|
|
if rotationStatus.OperationType == string(fleet.MDMOperationTypeRemove) {
|
|
return &fleet.BadRequestError{
|
|
Message: "Cannot rotate recovery lock password while a clear operation is in progress.",
|
|
}
|
|
}
|
|
|
|
// Validate: must have status verified or failed (not pending or NULL)
|
|
status := ""
|
|
if rotationStatus.Status != nil {
|
|
status = *rotationStatus.Status
|
|
}
|
|
if status != string(fleet.MDMDeliveryVerified) && status != string(fleet.MDMDeliveryFailed) {
|
|
return &fleet.BadRequestError{
|
|
Message: "Cannot rotate recovery lock password while an operation is pending.",
|
|
}
|
|
}
|
|
|
|
// Generate new password
|
|
newPassword := apple_mdm.GenerateRecoveryLockPassword()
|
|
|
|
// Store pending rotation
|
|
if err := svc.ds.InitiateRecoveryLockRotation(ctx, host.UUID, newPassword); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "initiate recovery lock rotation")
|
|
}
|
|
|
|
// Enqueue MDM command
|
|
cmdUUID := uuid.NewString()
|
|
if err := svc.mdmAppleCommander.RotateRecoveryLock(ctx, host.UUID, cmdUUID); err != nil {
|
|
// Only clear the pending rotation if the enqueue itself failed.
|
|
// If it's an APNS delivery error, the command was successfully enqueued
|
|
// and will be delivered when the device checks in.
|
|
var apnsErr *apple_mdm.APNSDeliveryError
|
|
if !errors.As(err, &apnsErr) {
|
|
_ = svc.ds.ClearRecoveryLockRotation(ctx, host.UUID)
|
|
}
|
|
return ctxerr.Wrap(ctx, err, "enqueue recovery lock rotation command")
|
|
}
|
|
|
|
// Log activity
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if ok {
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
vc.User,
|
|
fleet.ActivityTypeRotatedHostRecoveryLockPassword{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
},
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "create activity for rotate recovery lock password")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
//go:embed embedded_scripts/windows_lock.ps1
|
|
windowsLockScript []byte
|
|
//go:embed embedded_scripts/windows_unlock.ps1
|
|
windowsUnlockScript []byte
|
|
//go:embed embedded_scripts/linux_lock.sh
|
|
linuxLockScript []byte
|
|
//go:embed embedded_scripts/linux_unlock.sh
|
|
linuxUnlockScript []byte
|
|
//go:embed embedded_scripts/linux_wipe.sh
|
|
linuxWipeScript []byte
|
|
|
|
windowsWipeCommand = `
|
|
<Exec>
|
|
<CmdID>%s</CmdID>
|
|
<Item>
|
|
<Target>
|
|
<LocURI>./Device/Vendor/MSFT/RemoteWipe/%s</LocURI>
|
|
</Target>
|
|
<Meta>
|
|
<Format xmlns="syncml:metinf">chr</Format>
|
|
<Type>text/plain</Type>
|
|
</Meta>
|
|
<Data></Data>
|
|
</Item>
|
|
</Exec>`
|
|
)
|
|
|
|
func (svc *Service) GetHostManagedAccountPassword(ctx context.Context, hostID uint) (*fleet.HostManagedLocalAccountPassword, error) {
|
|
// First ensure the user has access to list hosts, then check the specific
|
|
// host once team_id is loaded.
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
return nil, err
|
|
}
|
|
host, err := svc.ds.HostLite(ctx, hostID)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "get host lite")
|
|
}
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
if !fleet.IsMacOSPlatform(host.Platform) {
|
|
return nil, &fleet.BadRequestError{
|
|
Message: "Host is not a macOS device.",
|
|
}
|
|
}
|
|
|
|
acct, err := svc.ds.GetHostManagedLocalAccountStatus(ctx, host.UUID)
|
|
if err != nil {
|
|
if fleet.IsNotFound(err) {
|
|
return nil, &fleet.BadRequestError{
|
|
Message: "Host does not have a managed account.",
|
|
}
|
|
}
|
|
return nil, ctxerr.Wrap(ctx, err, "get host managed account status")
|
|
}
|
|
if acct.Status == nil || *acct.Status != string(fleet.MDMDeliveryVerified) {
|
|
return nil, &fleet.BadRequestError{
|
|
Message: "Host's managed account password is not yet verified.",
|
|
}
|
|
}
|
|
|
|
pwd, err := svc.ds.GetHostManagedLocalAccountPassword(ctx, host.UUID)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "get host managed account password")
|
|
}
|
|
|
|
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeViewedManagedLocalAccount{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
}); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "create viewed managed local account activity")
|
|
}
|
|
|
|
return pwd, nil
|
|
}
|