mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #34529 # Details This PR implements the backend (and some related front-end screens) for allowing Fleet admins to require that users authenticate with an IdP prior to having their devices set up. I'll comment on changes inline but the high-level for the device enrollment flow is: 1. The handler for the `/orbit/enroll` endpoint now checks whether the end-user authentication is required for the team (or globally, if using the global enroll secret). 2. If so, it checks whether a `host_mdm_idp_accounts` row exists with a `host_uuid` matching the identifier sent with the request 3. If a row exists, enroll. If not, return back a new flavor of `OrbitError` with a `401` status code and a message (`END_USER_AUTH_REQUIRED`) that Orbit can interpret and act accordingly. Additionally some changes were made to the MDM SSO flow. Namely, adding more data to the session we store for correlating requests we make to the IdP to initiate SSO to responses aimed at our callback. We now store a `RequestData` struct which contains the UUID of the device making the request, as well as the "initiator" (in this case, "setup_experience"). When our SSO callback detects that the initiator was the setup experience, it attempts to add all of the relevant records to our database to associate the host with an IdP account. This removes the enrollment gate in the `/orbit/enroll` endpoint. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. Will put the changelog in the last ticket for the story - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [X] Added/updated automated tests will see if there's any more to update - [X] QA'd all new/changed functionality manually To test w/ SimpleSAML 1. Log in to your local Fleet dashboard with MDM and IdP set up for SimpleSAML 1. Go to Settings -> Integrations -> Identity provider 2. Use "SimpleSAML" for the provider name 3. Use `mdm.test.com` for the entity ID 4. Use `http://127.0.0.1:9080/simplesaml/saml2/idp/metadata.php` for the metadata URL 1. Set up a team (or "no team") to have End User Authentication required (Controls -> Setup experience) 1. Get the enroll secret of that team 1. In the browser console, do: ``` fetch("https://localhost:8080/api/fleet/orbit/enroll", { "headers": { "accept": "application/json, text/plain, */*", "cache-control": "no-cache", "content-type": "application/json", "pragma": "no-cache", }, "body": "{\"enroll_secret\":\"<enroll secret>", \"hardware_uuid\":\"abc123\" }", "method": "POST", }); ``` replacing `<enroll secret>` with your team's enroll secret. 8. Verify in the network tab that you get a 401 error with message `END_USER_AUTH_REQUIRED` 1. Go to https://localhost:8080/mdm/sso?initiator=setup_experience&host_uuid=abc123 1. Verify that a new screen appears asking you to log in to your IdP 1. Log in to SimpleSAML with `sso_user / user123#` 1. Verify that you're taken to a success screen 1. In your database, verify that records exist in the `mdm_idp_accounts` and `host_mdm_idp_accounts` tables with uuid `abc123` 1. Try the `fetch` command in the browser console again, verify that it succeeds. ## fleetd/orbit/Fleet Desktop - [ ] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) This is _not_ compatible with the current version of fleetd or the soon-to-be-released 1.49.x. Until #34847 changes are released in fleetd, this will need to be put behind a feature flag or withheld from Fleet releases. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added support for device UUID linkage during MDM enrollment to enable host-initiated enrollment tracking * Introduced setup experience flow for device authentication during enrollment * Added end-user authentication requirement configuration for macOS MDM enrollment * **Improvements** * Enhanced MDM enrollment process to maintain device context through authentication * Updated authentication UI to display completion status for device setup flows * Refined form layout styling for improved visual consistency <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1616 lines
55 KiB
Go
1616 lines
55 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig"
|
|
"github.com/fleetdm/fleet/v4/server"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/capabilities"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mdm"
|
|
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/service/contract"
|
|
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
|
|
"github.com/fleetdm/fleet/v4/server/worker"
|
|
"github.com/go-kit/log/level"
|
|
)
|
|
|
|
type setOrbitNodeKeyer interface {
|
|
setOrbitNodeKey(nodeKey string)
|
|
}
|
|
|
|
type EnrollOrbitResponse struct {
|
|
OrbitNodeKey string `json:"orbit_node_key,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
type orbitGetConfigRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
}
|
|
|
|
func (r *orbitGetConfigRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
func (r *orbitGetConfigRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
type orbitGetConfigResponse struct {
|
|
fleet.OrbitConfig
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r orbitGetConfigResponse) Error() error { return r.Err }
|
|
|
|
func (r EnrollOrbitResponse) Error() error { return r.Err }
|
|
|
|
// HijackRender so we can add a header with the server capabilities in the
|
|
// response, allowing Orbit to know what features are available without the
|
|
// need to enroll.
|
|
func (r EnrollOrbitResponse) HijackRender(ctx context.Context, w http.ResponseWriter) {
|
|
writeCapabilitiesHeader(w, fleet.GetServerOrbitCapabilities())
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
|
|
if err := enc.Encode(r); err != nil {
|
|
endpoint_utils.EncodeError(ctx, newOsqueryError(fmt.Sprintf("orbit enroll failed: %s", err)), w)
|
|
}
|
|
}
|
|
|
|
func enrollOrbitEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*contract.EnrollOrbitRequest)
|
|
nodeKey, err := svc.EnrollOrbit(ctx, fleet.OrbitHostInfo{
|
|
HardwareUUID: req.HardwareUUID,
|
|
HardwareSerial: req.HardwareSerial,
|
|
Hostname: req.Hostname,
|
|
Platform: req.Platform,
|
|
PlatformLike: req.PlatformLike,
|
|
OsqueryIdentifier: req.OsqueryIdentifier,
|
|
ComputerName: req.ComputerName,
|
|
HardwareModel: req.HardwareModel,
|
|
}, req.EnrollSecret)
|
|
if err != nil {
|
|
return EnrollOrbitResponse{Err: err}, nil
|
|
}
|
|
return EnrollOrbitResponse{OrbitNodeKey: nodeKey}, nil
|
|
}
|
|
|
|
func (svc *Service) AuthenticateOrbitHost(ctx context.Context, orbitNodeKey string) (*fleet.Host, bool, error) {
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
if orbitNodeKey == "" {
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: missing orbit node key"))
|
|
}
|
|
|
|
host, err := svc.ds.LoadHostByOrbitNodeKey(ctx, orbitNodeKey)
|
|
switch {
|
|
case err == nil:
|
|
// OK
|
|
case fleet.IsNotFound(err):
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: invalid orbit node key"))
|
|
default:
|
|
return nil, false, ctxerr.Wrap(ctx, err, "authentication error orbit")
|
|
}
|
|
|
|
if *host.HasHostIdentityCert {
|
|
err = httpsig.VerifyHostIdentity(ctx, svc.ds, host)
|
|
if err != nil {
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError(fmt.Sprintf("authentication error orbit: %s", err.Error())))
|
|
}
|
|
}
|
|
|
|
return host, svc.debugEnabledForHost(ctx, host.ID), nil
|
|
}
|
|
|
|
// EnrollOrbit enrolls an Orbit instance to Fleet and returns the orbit node key.
|
|
func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string) (string, error) {
|
|
// this is not a user-authenticated endpoint
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
logging.WithLevel(
|
|
logging.WithExtras(ctx,
|
|
"hardware_uuid", hostInfo.HardwareUUID,
|
|
"hardware_serial", hostInfo.HardwareSerial,
|
|
"hostname", hostInfo.Hostname,
|
|
"platform", hostInfo.Platform,
|
|
"platform_like", hostInfo.PlatformLike,
|
|
"osquery_identifier", hostInfo.OsqueryIdentifier,
|
|
"computer_name", hostInfo.ComputerName,
|
|
"hardware_model", hostInfo.HardwareModel,
|
|
),
|
|
level.Info,
|
|
)
|
|
|
|
secret, err := svc.ds.VerifyEnrollSecret(ctx, enrollSecret)
|
|
if err != nil {
|
|
if fleet.IsNotFound(err) {
|
|
// OK - This can happen if the following sequence of events take place:
|
|
// 1. User deletes global/team enroll secret.
|
|
// 2. User deletes the host in Fleet.
|
|
// 3. Orbit tries to re-enroll using old secret.
|
|
return "", fleet.NewAuthFailedError("invalid secret")
|
|
}
|
|
return "", fleet.OrbitError{Message: err.Error()}
|
|
}
|
|
|
|
identifier := hostInfo.OsqueryIdentifier
|
|
if identifier == "" {
|
|
identifier = hostInfo.HardwareUUID
|
|
}
|
|
|
|
identityCert, err := svc.ds.GetHostIdentityCertByName(ctx, identifier)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return "", fleet.OrbitError{Message: fmt.Sprintf("loading certificate: %s", err.Error())}
|
|
}
|
|
|
|
// If an identity certificate exists for this host, make sure the request had an HTTP message signature with the matching certificate.
|
|
hostIdentityCert, httpSigPresent := httpsig.FromContext(ctx)
|
|
if identityCert != nil {
|
|
if !httpSigPresent {
|
|
return "", fleet.NewAuthFailedError("authentication error: missing HTTP signature")
|
|
}
|
|
if identityCert.SerialNumber != hostIdentityCert.SerialNumber {
|
|
return "", fleet.NewAuthFailedError("authentication error: certificate serial number mismatch")
|
|
}
|
|
} else if httpSigPresent { // but we couldn't find the cert in DB
|
|
return "", fleet.NewAuthFailedError("authentication error: certificate matching HTTP message signature not found")
|
|
}
|
|
|
|
orbitNodeKey, err := server.GenerateRandomText(svc.config.Osquery.NodeKeySize)
|
|
if err != nil {
|
|
return "", fleet.OrbitError{Message: "failed to generate orbit node key: " + err.Error()}
|
|
}
|
|
|
|
appConfig, err := svc.ds.AppConfig(ctx)
|
|
if err != nil {
|
|
return "", fleet.OrbitError{Message: "app config load failed: " + err.Error()}
|
|
}
|
|
isEndUserAuthRequired := appConfig.MDM.MacOSSetup.EnableEndUserAuthentication
|
|
// If the secret is for a team, get the team config as well.
|
|
if secret.TeamID != nil {
|
|
team, err := svc.ds.Team(ctx, *secret.TeamID)
|
|
if err != nil {
|
|
return "", fleet.OrbitError{Message: "failed to get team config: " + err.Error()}
|
|
}
|
|
isEndUserAuthRequired = team.Config.MDM.MacOSSetup.EnableEndUserAuthentication
|
|
}
|
|
|
|
if isEndUserAuthRequired {
|
|
if hostInfo.HardwareUUID == "" {
|
|
return "", fleet.OrbitError{Message: "failed to get IdP account: hardware uuid is empty"}
|
|
}
|
|
// Try to find an IdP account for this host.
|
|
idpAccount, err := svc.ds.GetMDMIdPAccountByHostUUID(ctx, hostInfo.HardwareUUID)
|
|
if err != nil {
|
|
return "", fleet.OrbitError{Message: "failed to get IdP account: " + err.Error()}
|
|
}
|
|
if idpAccount == nil {
|
|
return "", fleet.NewOrbitIDPAuthRequiredError()
|
|
}
|
|
}
|
|
|
|
var stickyEnrollment *string
|
|
if svc.keyValueStore != nil {
|
|
// Check for sticky MDM enrollment flag. When set (e.g., after a host transfer),
|
|
// this prevents enrollment-based team changes for a time window to avoid race conditions
|
|
// with MDM profile delivery.
|
|
stickyEnrollment, err = svc.keyValueStore.Get(ctx, fleet.StickyMDMEnrollmentKeyPrefix+hostInfo.HardwareUUID)
|
|
if err != nil {
|
|
// Log error but continue enrollment (fail-open approach). If Redis is unavailable,
|
|
// enrollment proceeds without sticky behavior rather than blocking.
|
|
level.Error(svc.logger).Log("msg", "failed to get sticky enrollment", "err", err, "host_uuid", hostInfo.HardwareUUID)
|
|
}
|
|
}
|
|
|
|
host, err := svc.ds.EnrollOrbit(ctx,
|
|
fleet.WithEnrollOrbitMDMEnabled(appConfig.MDM.EnabledAndConfigured),
|
|
fleet.WithEnrollOrbitHostInfo(hostInfo),
|
|
fleet.WithEnrollOrbitNodeKey(orbitNodeKey),
|
|
fleet.WithEnrollOrbitTeamID(secret.TeamID),
|
|
fleet.WithEnrollOrbitIdentityCert(identityCert),
|
|
fleet.WithEnrollOrbitIgnoreTeamUpdate(stickyEnrollment != nil),
|
|
)
|
|
if err != nil {
|
|
return "", fleet.OrbitError{Message: "failed to enroll " + err.Error()}
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
nil,
|
|
fleet.ActivityTypeFleetEnrolled{
|
|
HostID: host.ID,
|
|
HostSerial: hostInfo.HardwareSerial,
|
|
HostDisplayName: host.DisplayName(),
|
|
},
|
|
); err != nil {
|
|
level.Error(svc.logger).Log("msg", "record fleet enroll activity", "err", err)
|
|
}
|
|
|
|
return orbitNodeKey, nil
|
|
}
|
|
|
|
func getOrbitConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
cfg, err := svc.GetOrbitConfig(ctx)
|
|
if err != nil {
|
|
return orbitGetConfigResponse{Err: err}, nil
|
|
}
|
|
return orbitGetConfigResponse{OrbitConfig: cfg}, nil
|
|
}
|
|
|
|
func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, error) {
|
|
// this is not a user-authenticated endpoint
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
return fleet.OrbitConfig{}, fleet.OrbitError{Message: "internal error: missing host from request context"}
|
|
}
|
|
|
|
appConfig, err := svc.ds.AppConfig(ctx)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
|
|
isConnectedToFleetMDM, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet")
|
|
}
|
|
|
|
mdmInfo, err := svc.ds.GetHostMDM(ctx, host.ID)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "retrieving host mdm info")
|
|
}
|
|
|
|
// set the host's orbit notifications for macOS MDM
|
|
var notifs fleet.OrbitConfigNotifications
|
|
if appConfig.MDM.EnabledAndConfigured && host.IsOsqueryEnrolled() && host.Platform == "darwin" {
|
|
needsDEPEnrollment := mdmInfo != nil && !mdmInfo.Enrolled && host.IsDEPAssignedToFleet()
|
|
|
|
if needsDEPEnrollment {
|
|
notifs.RenewEnrollmentProfile = true
|
|
}
|
|
|
|
manualMigrationEligible, err := fleet.IsEligibleForManualMigration(host, mdmInfo, isConnectedToFleetMDM)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "checking manual migration eligibility")
|
|
}
|
|
|
|
if appConfig.MDM.MacOSMigration.Enable &&
|
|
(fleet.IsEligibleForDEPMigration(host, mdmInfo, isConnectedToFleetMDM) || manualMigrationEligible) {
|
|
notifs.NeedsMDMMigration = true
|
|
}
|
|
|
|
if isConnectedToFleetMDM {
|
|
// If there is no software or script configured for setup experience and this is the
|
|
// first time orbit is calling the /config endpoint, then this host
|
|
// will not have a row in host_mdm_apple_awaiting_configuration.
|
|
// On subsequent calls to /config, the host WILL have a row in
|
|
// host_mdm_apple_awaiting_configuration.
|
|
inSetupAssistant, err := svc.ds.GetHostAwaitingConfiguration(ctx, host.UUID)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "checking if host is in setup experience")
|
|
}
|
|
|
|
if inSetupAssistant {
|
|
notifs.RunSetupExperience = true
|
|
}
|
|
|
|
if inSetupAssistant {
|
|
// If the client is running a fleetd that doesn't support setup
|
|
// experience, then we should fall back to the "old way" of releasing
|
|
// the device.
|
|
mp, ok := capabilities.FromContext(ctx)
|
|
if !ok || !mp.Has(fleet.CapabilitySetupExperience) {
|
|
level.Debug(svc.logger).Log("msg", "host doesn't support setup experience, falling back to worker-based device release", "host_uuid", host.UUID)
|
|
if err := svc.processReleaseDeviceForOldFleetd(ctx, host); err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// set the host's orbit notifications for Windows MDM
|
|
if appConfig.MDM.WindowsEnabledAndConfigured {
|
|
if isEligibleForWindowsMDMEnrollment(host, mdmInfo) {
|
|
discoURL, err := microsoft_mdm.ResolveWindowsMDMDiscovery(appConfig.ServerSettings.ServerURL)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
notifs.WindowsMDMDiscoveryEndpoint = discoURL
|
|
notifs.NeedsProgrammaticWindowsMDMEnrollment = true
|
|
} else if appConfig.MDM.WindowsMigrationEnabled && isEligibleForWindowsMDMMigration(host, mdmInfo) {
|
|
notifs.NeedsMDMMigration = true
|
|
|
|
// Set the host to refetch the "critical queries" quickly for some time,
|
|
// to improve ingestion time of the unenroll and make the host eligible to
|
|
// enroll into Fleet faster.
|
|
if host.RefetchCriticalQueriesUntil == nil {
|
|
refetchUntil := svc.clock.Now().Add(fleet.RefetchMDMUnenrollCriticalQueryDuration)
|
|
host.RefetchCriticalQueriesUntil = &refetchUntil
|
|
if err := svc.ds.UpdateHostRefetchCriticalQueriesUntil(ctx, host.ID, &refetchUntil); err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !appConfig.MDM.WindowsEnabledAndConfigured {
|
|
if host.IsEligibleForWindowsMDMUnenrollment(isConnectedToFleetMDM) {
|
|
notifs.NeedsProgrammaticWindowsMDMUnenrollment = true
|
|
}
|
|
}
|
|
|
|
// load the (active, ready to execute) pending script executions for that host
|
|
pending, err := svc.ds.ListReadyToExecuteScriptsForHost(ctx, host.ID, appConfig.ServerSettings.ScriptsDisabled)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
if len(pending) > 0 {
|
|
execIDs := make([]string, 0, len(pending))
|
|
for _, p := range pending {
|
|
execIDs = append(execIDs, p.ExecutionID)
|
|
}
|
|
notifs.PendingScriptExecutionIDs = execIDs
|
|
}
|
|
|
|
notifs.RunDiskEncryptionEscrow = host.IsLUKSSupported() &&
|
|
host.DiskEncryptionEnabled != nil &&
|
|
*host.DiskEncryptionEnabled &&
|
|
svc.ds.IsHostPendingEscrow(ctx, host.ID)
|
|
|
|
// load the (active, ready to execute) pending software install executions for that host
|
|
pendingInstalls, err := svc.ds.ListReadyToExecuteSoftwareInstalls(ctx, host.ID)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
if len(pendingInstalls) > 0 {
|
|
notifs.PendingSoftwareInstallerIDs = pendingInstalls
|
|
}
|
|
|
|
// team ID is not nil, get team specific flags and options
|
|
if host.TeamID != nil {
|
|
teamAgentOptions, err := svc.ds.TeamAgentOptions(ctx, *host.TeamID)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
|
|
var opts fleet.AgentOptions
|
|
if teamAgentOptions != nil && len(*teamAgentOptions) > 0 {
|
|
if err := json.Unmarshal(*teamAgentOptions, &opts); err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
}
|
|
|
|
extensionsFiltered, err := svc.filterExtensionsForHost(ctx, opts.Extensions, host)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
|
|
mdmConfig, err := svc.ds.TeamMDMConfig(ctx, *host.TeamID)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
|
|
var nudgeConfig *fleet.NudgeConfig
|
|
if appConfig.MDM.EnabledAndConfigured &&
|
|
mdmConfig != nil &&
|
|
host.IsOsqueryEnrolled() &&
|
|
isConnectedToFleetMDM &&
|
|
mdmConfig.MacOSUpdates.Configured() {
|
|
|
|
hostOS, err := svc.ds.GetHostOperatingSystem(ctx, host.ID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
// host os has not been collected yet (no details query)
|
|
hostOS = &fleet.OperatingSystem{}
|
|
} else if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
requiresNudge, err := hostOS.RequiresNudge()
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
|
|
if requiresNudge {
|
|
nudgeConfig, err = fleet.NewNudgeConfig(mdmConfig.MacOSUpdates)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
}
|
|
}
|
|
|
|
err = svc.setDiskEncryptionNotifications(
|
|
ctx,
|
|
¬ifs,
|
|
host,
|
|
appConfig,
|
|
mdmConfig.EnableDiskEncryption,
|
|
isConnectedToFleetMDM,
|
|
mdmInfo,
|
|
)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "setting team disk encryption notifications")
|
|
}
|
|
|
|
var updateChannels *fleet.OrbitUpdateChannels
|
|
if len(opts.UpdateChannels) > 0 {
|
|
var uc fleet.OrbitUpdateChannels
|
|
if err := json.Unmarshal(opts.UpdateChannels, &uc); err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
updateChannels = &uc
|
|
}
|
|
|
|
// only unset this flag once we know there were no errors so this notification will be picked up by the agent
|
|
if notifs.RunDiskEncryptionEscrow {
|
|
_ = svc.ds.ClearPendingEscrow(ctx, host.ID)
|
|
}
|
|
|
|
return fleet.OrbitConfig{
|
|
ScriptExeTimeout: opts.ScriptExecutionTimeout,
|
|
Flags: opts.CommandLineStartUpFlags,
|
|
Extensions: extensionsFiltered,
|
|
Notifications: notifs,
|
|
NudgeConfig: nudgeConfig,
|
|
UpdateChannels: updateChannels,
|
|
}, nil
|
|
}
|
|
|
|
// team ID is nil, get global flags and options
|
|
var opts fleet.AgentOptions
|
|
if appConfig.AgentOptions != nil {
|
|
if err := json.Unmarshal(*appConfig.AgentOptions, &opts); err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
}
|
|
|
|
extensionsFiltered, err := svc.filterExtensionsForHost(ctx, opts.Extensions, host)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
|
|
var nudgeConfig *fleet.NudgeConfig
|
|
if appConfig.MDM.EnabledAndConfigured &&
|
|
isConnectedToFleetMDM &&
|
|
host.IsOsqueryEnrolled() &&
|
|
appConfig.MDM.MacOSUpdates.Configured() {
|
|
hostOS, err := svc.ds.GetHostOperatingSystem(ctx, host.ID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
// host os has not been collected yet (no details query)
|
|
hostOS = &fleet.OperatingSystem{}
|
|
} else if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
requiresNudge, err := hostOS.RequiresNudge()
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
|
|
if requiresNudge {
|
|
nudgeConfig, err = fleet.NewNudgeConfig(appConfig.MDM.MacOSUpdates)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
}
|
|
}
|
|
|
|
err = svc.setDiskEncryptionNotifications(
|
|
ctx,
|
|
¬ifs,
|
|
host,
|
|
appConfig,
|
|
appConfig.MDM.EnableDiskEncryption.Value,
|
|
isConnectedToFleetMDM,
|
|
mdmInfo,
|
|
)
|
|
if err != nil {
|
|
return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "setting no-team disk encryption notifications")
|
|
}
|
|
|
|
var updateChannels *fleet.OrbitUpdateChannels
|
|
if len(opts.UpdateChannels) > 0 {
|
|
var uc fleet.OrbitUpdateChannels
|
|
if err := json.Unmarshal(opts.UpdateChannels, &uc); err != nil {
|
|
return fleet.OrbitConfig{}, err
|
|
}
|
|
updateChannels = &uc
|
|
}
|
|
|
|
// only unset this flag once we know there were no errors so this notification will be picked up by the agent
|
|
if notifs.RunDiskEncryptionEscrow {
|
|
_ = svc.ds.ClearPendingEscrow(ctx, host.ID)
|
|
}
|
|
|
|
return fleet.OrbitConfig{
|
|
ScriptExeTimeout: opts.ScriptExecutionTimeout,
|
|
Flags: opts.CommandLineStartUpFlags,
|
|
Extensions: extensionsFiltered,
|
|
Notifications: notifs,
|
|
NudgeConfig: nudgeConfig,
|
|
UpdateChannels: updateChannels,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) processReleaseDeviceForOldFleetd(ctx context.Context, host *fleet.Host) error {
|
|
var manualRelease bool
|
|
if host.TeamID == nil {
|
|
ac, err := svc.ds.AppConfig(ctx)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get AppConfig to read enable_release_device_manually")
|
|
}
|
|
manualRelease = ac.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
|
|
} else {
|
|
tm, err := svc.ds.Team(ctx, *host.TeamID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get Team to read enable_release_device_manually")
|
|
}
|
|
manualRelease = tm.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
|
|
}
|
|
|
|
if !manualRelease {
|
|
// For the commands to await, since we're in an orbit endpoint we know that
|
|
// fleetd has already been installed, so we only need to check for the
|
|
// bootstrap package install and the SSO account configuration (both are
|
|
// optional).
|
|
bootstrapCmdUUID, err := svc.ds.GetHostBootstrapPackageCommand(ctx, host.UUID)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return ctxerr.Wrap(ctx, err, "get bootstrap package command")
|
|
}
|
|
|
|
// 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 ctxerr.Wrap(ctx, err, "list AccountConfiguration commands")
|
|
}
|
|
var acctConfigCmdUUID string
|
|
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.
|
|
acctConfigCmdUUID = acctCmds[0].CommandUUID
|
|
}
|
|
|
|
// Enroll reference arg is not used in the release device task, passing empty string.
|
|
if err := worker.QueueAppleMDMJob(ctx, svc.ds, svc.logger, worker.AppleMDMPostDEPReleaseDeviceTask,
|
|
host.UUID, host.Platform, host.TeamID, "", false, bootstrapCmdUUID, acctConfigCmdUUID); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job")
|
|
}
|
|
}
|
|
|
|
// at this point we know for sure that it will get released, but we need to
|
|
// ensure we won't continually enqueue new worker jobs for that host until it
|
|
// is released. To do so, we clear up the setup experience data (since anyway
|
|
// this host will not go through that new flow).
|
|
if err := svc.ds.SetHostAwaitingConfiguration(ctx, host.UUID, false); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "unset host awaiting configuration")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc *Service) setDiskEncryptionNotifications(
|
|
ctx context.Context,
|
|
notifs *fleet.OrbitConfigNotifications,
|
|
host *fleet.Host,
|
|
appConfig *fleet.AppConfig,
|
|
diskEncryptionConfigured bool,
|
|
isConnectedToFleetMDM bool,
|
|
mdmInfo *fleet.HostMDM,
|
|
) error {
|
|
anyMDMConfigured := appConfig.MDM.EnabledAndConfigured || appConfig.MDM.WindowsEnabledAndConfigured
|
|
if !anyMDMConfigured ||
|
|
!isConnectedToFleetMDM ||
|
|
!host.IsOsqueryEnrolled() ||
|
|
!diskEncryptionConfigured {
|
|
return nil
|
|
}
|
|
|
|
encryptionKey, err := svc.ds.GetHostDiskEncryptionKey(ctx, host.ID)
|
|
if err != nil {
|
|
if !fleet.IsNotFound(err) {
|
|
return ctxerr.Wrap(ctx, err, "fetching host disk encryption key")
|
|
}
|
|
}
|
|
|
|
switch host.FleetPlatform() {
|
|
case "darwin":
|
|
mp, ok := capabilities.FromContext(ctx)
|
|
if !ok {
|
|
level.Debug(svc.logger).Log("msg", "no capabilities in context, skipping disk encryption notification")
|
|
return nil
|
|
}
|
|
|
|
if !mp.Has(fleet.CapabilityEscrowBuddy) {
|
|
level.Debug(svc.logger).Log("msg", "host doesn't support Escrow Buddy, skipping disk encryption notification", "host_uuid", host.UUID)
|
|
return nil
|
|
}
|
|
|
|
notifs.RotateDiskEncryptionKey = encryptionKey != nil && encryptionKey.Decryptable != nil && !*encryptionKey.Decryptable
|
|
case "windows":
|
|
isServer := mdmInfo != nil && mdmInfo.IsServer
|
|
needsEncryption := host.DiskEncryptionEnabled != nil && !*host.DiskEncryptionEnabled
|
|
keyWasDecrypted := encryptionKey != nil && encryptionKey.Decryptable != nil && *encryptionKey.Decryptable
|
|
encryptedWithoutKey := host.DiskEncryptionEnabled != nil && *host.DiskEncryptionEnabled && !keyWasDecrypted
|
|
notifs.EnforceBitLockerEncryption = !isServer &&
|
|
mdmInfo != nil &&
|
|
(needsEncryption || encryptedWithoutKey)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// filterExtensionsForHost filters a extensions configuration depending on the host platform and label membership.
|
|
//
|
|
// If all extensions are filtered, then it returns (nil, nil) (Orbit expects empty extensions if there
|
|
// are no extensions for the host.)
|
|
func (svc *Service) filterExtensionsForHost(ctx context.Context, extensions json.RawMessage, host *fleet.Host) (json.RawMessage, error) {
|
|
if len(extensions) == 0 {
|
|
return nil, nil
|
|
}
|
|
var extensionsInfo fleet.Extensions
|
|
if err := json.Unmarshal(extensions, &extensionsInfo); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "unmarshal extensions config")
|
|
}
|
|
|
|
// Filter the extensions by platform.
|
|
extensionsInfo.FilterByHostPlatform(host.Platform, host.CPUType)
|
|
|
|
// Filter the extensions by labels (premium only feature).
|
|
if license, _ := license.FromContext(ctx); license != nil && license.IsPremium() {
|
|
for extensionName, extensionInfo := range extensionsInfo {
|
|
hostIsMemberOfAllLabels, err := svc.ds.HostMemberOfAllLabels(ctx, host.ID, extensionInfo.Labels)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "check host labels")
|
|
}
|
|
if hostIsMemberOfAllLabels {
|
|
// Do not filter out, but there's no need to send the label names to the devices.
|
|
extensionInfo.Labels = nil
|
|
extensionsInfo[extensionName] = extensionInfo
|
|
} else {
|
|
delete(extensionsInfo, extensionName)
|
|
}
|
|
}
|
|
}
|
|
// Orbit expects empty message if no extensions apply.
|
|
if len(extensionsInfo) == 0 {
|
|
return nil, nil
|
|
}
|
|
extensionsFiltered, err := json.Marshal(extensionsInfo)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "marshal extensions config")
|
|
}
|
|
return extensionsFiltered, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Ping orbit endpoint
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type orbitPingRequest struct{}
|
|
|
|
type orbitPingResponse struct{}
|
|
|
|
func (r orbitPingResponse) HijackRender(ctx context.Context, w http.ResponseWriter) {
|
|
writeCapabilitiesHeader(w, fleet.GetServerOrbitCapabilities())
|
|
}
|
|
|
|
func (r orbitPingResponse) Error() error { return nil }
|
|
|
|
// NOTE: we're intentionally not reading the capabilities header in this
|
|
// endpoint as is unauthenticated and we don't want to trust whatever comes in
|
|
// there.
|
|
func orbitPingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
svc.DisableAuthForPing(ctx)
|
|
return orbitPingResponse{}, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// SetOrUpdateDeviceToken endpoint
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type setOrUpdateDeviceTokenRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
DeviceAuthToken string `json:"device_auth_token"`
|
|
}
|
|
|
|
func (r *setOrUpdateDeviceTokenRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
func (r *setOrUpdateDeviceTokenRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
type setOrUpdateDeviceTokenResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r setOrUpdateDeviceTokenResponse) Error() error { return r.Err }
|
|
|
|
func setOrUpdateDeviceTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*setOrUpdateDeviceTokenRequest)
|
|
if err := svc.SetOrUpdateDeviceAuthToken(ctx, req.DeviceAuthToken); err != nil {
|
|
return setOrUpdateDeviceTokenResponse{Err: err}, nil
|
|
}
|
|
return setOrUpdateDeviceTokenResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) SetOrUpdateDeviceAuthToken(ctx context.Context, deviceAuthToken string) error {
|
|
// this is not a user-authenticated endpoint
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
if len(deviceAuthToken) == 0 {
|
|
return badRequest("device auth token cannot be empty")
|
|
}
|
|
|
|
if url.QueryEscape(deviceAuthToken) != deviceAuthToken {
|
|
return badRequest("device auth token contains invalid characters")
|
|
}
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
return newOsqueryError("internal error: missing host from request context")
|
|
}
|
|
|
|
if err := svc.ds.SetOrUpdateDeviceAuthToken(ctx, host.ID, deviceAuthToken); err != nil {
|
|
if errors.As(err, &fleet.ConflictError{}) {
|
|
return err
|
|
}
|
|
return newOsqueryError(fmt.Sprintf("internal error: failed to set or update device auth token: %s", err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Get Orbit pending script execution request
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type orbitGetScriptRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
ExecutionID string `json:"execution_id"`
|
|
}
|
|
|
|
// interface implementation required by the OrbitClient
|
|
func (r *orbitGetScriptRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
// interface implementation required by orbit authentication
|
|
func (r *orbitGetScriptRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
type orbitGetScriptResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
*fleet.HostScriptResult
|
|
}
|
|
|
|
func (r orbitGetScriptResponse) Error() error { return r.Err }
|
|
|
|
func getOrbitScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*orbitGetScriptRequest)
|
|
script, err := svc.GetHostScript(ctx, req.ExecutionID)
|
|
if err != nil {
|
|
return orbitGetScriptResponse{Err: err}, nil
|
|
}
|
|
return orbitGetScriptResponse{HostScriptResult: script}, nil
|
|
}
|
|
|
|
func (svc *Service) GetHostScript(ctx context.Context, execID string) (*fleet.HostScriptResult, error) {
|
|
// this is not a user-authenticated endpoint
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
return nil, fleet.OrbitError{Message: "internal error: missing host from request context"}
|
|
}
|
|
|
|
// get the script's details
|
|
script, err := svc.ds.GetHostScriptExecutionResult(ctx, execID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// ensure it cannot get access to a different host's script
|
|
if script.HostID != host.ID {
|
|
return nil, ctxerr.Wrap(ctx, newNotFoundError(), "no script found for this host")
|
|
}
|
|
|
|
// We expose secret variables in the script content to the host. The exposed secrets are only intended to go to the device and not accessible via the UI/API.
|
|
script.ScriptContents, err = svc.ds.ExpandEmbeddedSecrets(ctx, script.ScriptContents)
|
|
if err != nil {
|
|
// This error should never occur because we validate secret variables on script upload.
|
|
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("expand embedded secrets for host %d and script %s", host.ID, execID))
|
|
}
|
|
|
|
return script, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Post Orbit script execution result
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type orbitPostScriptResultRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
*fleet.HostScriptResultPayload
|
|
}
|
|
|
|
// interface implementation required by the OrbitClient
|
|
func (r *orbitPostScriptResultRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
// interface implementation required by orbit authentication
|
|
func (r *orbitPostScriptResultRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
type orbitPostScriptResultResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r orbitPostScriptResultResponse) Error() error { return r.Err }
|
|
|
|
func postOrbitScriptResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*orbitPostScriptResultRequest)
|
|
if err := svc.SaveHostScriptResult(ctx, req.HostScriptResultPayload); err != nil {
|
|
return orbitPostScriptResultResponse{Err: err}, nil
|
|
}
|
|
return orbitPostScriptResultResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.HostScriptResultPayload) error {
|
|
// this is not a user-authenticated endpoint
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
return fleet.OrbitError{Message: "internal error: missing host from request context"}
|
|
}
|
|
if result == nil {
|
|
return ctxerr.Wrap(ctx, &fleet.BadRequestError{Message: "missing script result"}, "save host script result")
|
|
}
|
|
|
|
// always use the authenticated host's ID as host_id
|
|
result.HostID = host.ID
|
|
hsr, action, err := svc.ds.SetHostScriptExecutionResult(ctx, result)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "save host script result")
|
|
}
|
|
|
|
// FIXME: datastore implementation of action seems rather brittle, can it be refactored?
|
|
var fromSetupExperience bool
|
|
if action == "" && fleet.IsSetupExperienceSupported(host.Platform) {
|
|
// this might be a setup experience script result
|
|
if updated, err := maybeUpdateSetupExperienceStatus(ctx, svc.ds, fleet.SetupExperienceScriptResult{
|
|
HostUUID: host.UUID,
|
|
ExecutionID: result.ExecutionID,
|
|
ExitCode: result.ExitCode,
|
|
}, true); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "update setup experience status")
|
|
} else if updated {
|
|
level.Debug(svc.logger).Log("msg", "setup experience script result updated", "host_uuid", host.UUID, "execution_id", result.ExecutionID)
|
|
fromSetupExperience = true
|
|
_, err := svc.EnterpriseOverrides.SetupExperienceNextStep(ctx, host)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "getting next step for host setup experience")
|
|
}
|
|
}
|
|
}
|
|
|
|
// don't create a "past" activity if the result was for a canceled activity
|
|
if hsr != nil && !hsr.Canceled {
|
|
var user *fleet.User
|
|
if hsr.UserID != nil {
|
|
user, err = svc.ds.UserByID(ctx, *hsr.UserID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get host script execution user")
|
|
}
|
|
}
|
|
var scriptName string
|
|
|
|
switch {
|
|
case hsr.ScriptID != nil:
|
|
scr, err := svc.ds.Script(ctx, *hsr.ScriptID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get saved script")
|
|
}
|
|
scriptName = scr.Name
|
|
case hsr.SetupExperienceScriptID != nil:
|
|
scr, err := svc.ds.GetSetupExperienceScriptByID(ctx, *hsr.SetupExperienceScriptID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get setup experience script")
|
|
}
|
|
|
|
scriptName = scr.Name
|
|
}
|
|
|
|
switch action {
|
|
case "uninstall":
|
|
softwareTitleName, selfService, err := svc.ds.GetDetailsForUninstallFromExecutionID(ctx, hsr.ExecutionID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get software title from execution ID")
|
|
}
|
|
activityStatus := "failed"
|
|
if hsr.ExitCode != nil && *hsr.ExitCode == 0 {
|
|
activityStatus = "uninstalled"
|
|
}
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
user,
|
|
fleet.ActivityTypeUninstalledSoftware{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
SoftwareTitle: softwareTitleName,
|
|
ExecutionID: hsr.ExecutionID,
|
|
Status: activityStatus,
|
|
SelfService: selfService,
|
|
},
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "create activity for script execution request")
|
|
}
|
|
|
|
// lastly, queue a vitals refetch so we get a proper view of inventory from osquery
|
|
if activityStatus == "uninstalled" {
|
|
if err := svc.ds.UpdateHostRefetchRequested(ctx, host.ID, true); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "queue host vitals refetch")
|
|
}
|
|
}
|
|
default:
|
|
// TODO(sarah): We may need to special case lock/unlock script results here?
|
|
var policyName *string
|
|
if hsr.PolicyID != nil {
|
|
if policy, err := svc.ds.PolicyLite(ctx, *hsr.PolicyID); err == nil {
|
|
policyName = &policy.Name // fall back to blank policy name if we can't retrieve the policy
|
|
}
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
user,
|
|
fleet.ActivityTypeRanScript{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
ScriptExecutionID: hsr.ExecutionID,
|
|
BatchExecutionID: hsr.BatchExecutionID,
|
|
ScriptName: scriptName,
|
|
Async: !hsr.SyncRequest,
|
|
PolicyID: hsr.PolicyID,
|
|
PolicyName: policyName,
|
|
FromSetupExperience: fromSetupExperience,
|
|
},
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "create activity for script execution request")
|
|
}
|
|
}
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Post Orbit device mapping (custom email)
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type orbitPutDeviceMappingRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
// interface implementation required by the OrbitClient
|
|
func (r *orbitPutDeviceMappingRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
// interface implementation required by orbit authentication
|
|
func (r *orbitPutDeviceMappingRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
type orbitPutDeviceMappingResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r orbitPutDeviceMappingResponse) Error() error { return r.Err }
|
|
|
|
func putOrbitDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*orbitPutDeviceMappingRequest)
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := newOsqueryError("internal error: missing host from request context")
|
|
return orbitPutDeviceMappingResponse{Err: err}, nil
|
|
}
|
|
|
|
_, err := svc.SetHostDeviceMapping(ctx, host.ID, req.Email, fleet.DeviceMappingCustomReplacement)
|
|
return orbitPutDeviceMappingResponse{Err: err}, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Post Orbit disk encryption key
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type orbitPostDiskEncryptionKeyRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
EncryptionKey []byte `json:"encryption_key"`
|
|
ClientError string `json:"client_error"`
|
|
}
|
|
|
|
// interface implementation required by the OrbitClient
|
|
func (r *orbitPostDiskEncryptionKeyRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
// interface implementation required by orbit authentication
|
|
func (r *orbitPostDiskEncryptionKeyRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
type orbitPostDiskEncryptionKeyResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r orbitPostDiskEncryptionKeyResponse) Error() error { return r.Err }
|
|
func (r orbitPostDiskEncryptionKeyResponse) Status() int { return http.StatusNoContent }
|
|
|
|
func postOrbitDiskEncryptionKeyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*orbitPostDiskEncryptionKeyRequest)
|
|
if err := svc.SetOrUpdateDiskEncryptionKey(ctx, string(req.EncryptionKey), req.ClientError); err != nil {
|
|
return orbitPostDiskEncryptionKeyResponse{Err: err}, nil
|
|
}
|
|
return orbitPostDiskEncryptionKeyResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) SetOrUpdateDiskEncryptionKey(ctx context.Context, encryptionKey, clientError string) error {
|
|
// this is not a user-authenticated endpoint
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
return newOsqueryError("internal error: missing host from request context")
|
|
}
|
|
|
|
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 badRequest("host is not enrolled with fleet")
|
|
}
|
|
|
|
var (
|
|
encryptedEncryptionKey string
|
|
decryptable *bool
|
|
)
|
|
|
|
// only set the encryption key if there was no client error
|
|
if clientError == "" && encryptionKey != "" {
|
|
wstepCert, _, _, err := svc.config.MDM.MicrosoftWSTEP()
|
|
if err != nil {
|
|
// should never return an error because the WSTEP is first parsed and
|
|
// cached at the start of the fleet serve process.
|
|
return ctxerr.Wrap(ctx, err, "get WSTEP certificate")
|
|
}
|
|
enc, err := microsoft_mdm.Encrypt(encryptionKey, wstepCert.Leaf)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "encrypt the key with WSTEP certificate")
|
|
}
|
|
encryptedEncryptionKey = enc
|
|
decryptable = ptr.Bool(true)
|
|
}
|
|
|
|
keyArchived, err := svc.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, encryptedEncryptionKey, clientError, decryptable)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "set or update disk encryption key")
|
|
}
|
|
|
|
// We only want to record the activity if the key was successfully archived.
|
|
if !keyArchived {
|
|
return nil
|
|
}
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
nil,
|
|
fleet.ActivityTypeEscrowedDiskEncryptionKey{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
},
|
|
); err != nil {
|
|
// OK: this is not critical to the operation of the endpoint
|
|
level.Error(svc.logger).Log(
|
|
"msg", "record fleet disk encryption key escrowed activity",
|
|
"err", err,
|
|
)
|
|
ctxerr.Handle(ctx, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Post Orbit LUKS (Linux disk encryption) data
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type orbitPostLUKSRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
Passphrase string `json:"passphrase"`
|
|
Salt string `json:"salt"`
|
|
KeySlot *uint `json:"key_slot"`
|
|
ClientError string `json:"client_error"`
|
|
}
|
|
|
|
// interface implementation required by the OrbitClient
|
|
func (r *orbitPostLUKSRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
// interface implementation required by orbit authentication
|
|
func (r *orbitPostLUKSRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
type orbitPostLUKSResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r orbitPostLUKSResponse) Error() error { return r.Err }
|
|
func (r orbitPostLUKSResponse) Status() int { return http.StatusNoContent }
|
|
|
|
func postOrbitLUKSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*orbitPostLUKSRequest)
|
|
if err := svc.EscrowLUKSData(ctx, req.Passphrase, req.Salt, req.KeySlot, req.ClientError); err != nil {
|
|
return orbitPostLUKSResponse{Err: err}, nil
|
|
}
|
|
return orbitPostLUKSResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) EscrowLUKSData(ctx context.Context, passphrase string, salt string, keySlot *uint, clientError string) error {
|
|
// this is not a user-authenticated endpoint
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
return newOsqueryError("internal error: missing host from request context")
|
|
}
|
|
|
|
if clientError != "" {
|
|
return svc.ds.ReportEscrowError(ctx, host.ID, clientError)
|
|
}
|
|
|
|
encryptedPassphrase, encryptedSalt, validatedKeySlot, err := svc.validateAndEncrypt(ctx, passphrase, salt, keySlot)
|
|
if err != nil {
|
|
_ = svc.ds.ReportEscrowError(ctx, host.ID, err.Error())
|
|
return err
|
|
}
|
|
|
|
keyArchived, err := svc.ds.SaveLUKSData(ctx, host, encryptedPassphrase, encryptedSalt, validatedKeySlot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// When only want to record a new activity if the current key was archived ...
|
|
if !keyArchived {
|
|
return nil
|
|
}
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
nil,
|
|
fleet.ActivityTypeEscrowedDiskEncryptionKey{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
},
|
|
); err != nil {
|
|
// OK: this is not critical to the operation of the endpoint
|
|
level.Error(svc.logger).Log(
|
|
"msg", "record fleet disk encryption key escrowed activity",
|
|
"err", err,
|
|
)
|
|
ctxerr.Handle(ctx, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc *Service) validateAndEncrypt(ctx context.Context, passphrase string, salt string, keySlot *uint) (encryptedPassphrase string, encryptedSalt string, validatedKeySlot uint, err error) {
|
|
if passphrase == "" || salt == "" || keySlot == nil {
|
|
return "", "", 0, badRequest("passphrase, salt, and key_slot must be provided to escrow LUKS data")
|
|
}
|
|
if svc.config.Server.PrivateKey == "" {
|
|
return "", "", 0, newOsqueryError("internal error: missing server private key")
|
|
}
|
|
|
|
encryptedPassphrase, err = mdm.EncryptAndEncode(passphrase, svc.config.Server.PrivateKey)
|
|
if err != nil {
|
|
return "", "", 0, ctxerr.Wrap(ctx, err, "internal error: could not encrypt LUKS data")
|
|
}
|
|
encryptedSalt, err = mdm.EncryptAndEncode(salt, svc.config.Server.PrivateKey)
|
|
if err != nil {
|
|
return "", "", 0, ctxerr.Wrap(ctx, err, "internal error: could not encrypt LUKS data")
|
|
}
|
|
|
|
return encryptedPassphrase, encryptedSalt, *keySlot, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Get Orbit pending software installations
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type orbitGetSoftwareInstallRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
OrbotNodeKey string `json:"orbot_node_key"` // legacy typo -- keep for backwards compatibility with orbit <= 1.38.0
|
|
InstallUUID string `json:"install_uuid"`
|
|
}
|
|
|
|
// interface implementation required by the OrbitClient
|
|
func (r *orbitGetSoftwareInstallRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
r.OrbotNodeKey = nodeKey // legacy typo -- keep for backwards compatability with fleet server < 4.63.0
|
|
}
|
|
|
|
// interface implementation required by the OrbitClient
|
|
func (r *orbitGetSoftwareInstallRequest) orbitHostNodeKey() string {
|
|
if r.OrbitNodeKey != "" {
|
|
return r.OrbitNodeKey
|
|
}
|
|
return r.OrbotNodeKey
|
|
}
|
|
|
|
type orbitGetSoftwareInstallResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
*fleet.SoftwareInstallDetails
|
|
}
|
|
|
|
func (r orbitGetSoftwareInstallResponse) Error() error { return r.Err }
|
|
|
|
func getOrbitSoftwareInstallDetails(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*orbitGetSoftwareInstallRequest)
|
|
details, err := svc.GetSoftwareInstallDetails(ctx, req.InstallUUID)
|
|
if err != nil {
|
|
return orbitGetSoftwareInstallResponse{Err: err}, nil
|
|
}
|
|
|
|
return orbitGetSoftwareInstallResponse{SoftwareInstallDetails: details}, nil
|
|
}
|
|
|
|
func (svc *Service) GetSoftwareInstallDetails(ctx context.Context, installUUID string) (*fleet.SoftwareInstallDetails, error) {
|
|
// this is not a user-authenticated endpoint
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
return nil, fleet.OrbitError{Message: "internal error: missing host from request context"}
|
|
}
|
|
|
|
details, err := svc.ds.GetSoftwareInstallDetails(ctx, installUUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// ensure it cannot get access to a different host's installers
|
|
if details.HostID != host.ID {
|
|
return nil, ctxerr.Wrap(ctx, newNotFoundError(), "no installer found for this host")
|
|
}
|
|
return details, nil
|
|
}
|
|
|
|
// Download Orbit software installer request
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type orbitDownloadSoftwareInstallerRequest struct {
|
|
Alt string `query:"alt"`
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
InstallerID uint `json:"installer_id"`
|
|
}
|
|
|
|
// interface implementation required by the OrbitClient
|
|
func (r *orbitDownloadSoftwareInstallerRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
// interface implementation required by orbit authentication
|
|
func (r *orbitDownloadSoftwareInstallerRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
func orbitDownloadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*orbitDownloadSoftwareInstallerRequest)
|
|
|
|
downloadRequested := req.Alt == "media"
|
|
if !downloadRequested {
|
|
// TODO: confirm error handling
|
|
return orbitDownloadSoftwareInstallerResponse{Err: &fleet.BadRequestError{Message: "only alt=media is supported"}}, nil
|
|
}
|
|
|
|
p, err := svc.OrbitDownloadSoftwareInstaller(ctx, req.InstallerID)
|
|
if err != nil {
|
|
return orbitDownloadSoftwareInstallerResponse{Err: err}, nil
|
|
}
|
|
return orbitDownloadSoftwareInstallerResponse{payload: p}, nil
|
|
}
|
|
|
|
func (svc *Service) OrbitDownloadSoftwareInstaller(ctx context.Context, installerID uint) (*fleet.DownloadSoftwareInstallerPayload, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Post Orbit software install result
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type orbitPostSoftwareInstallResultRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
*fleet.HostSoftwareInstallResultPayload
|
|
}
|
|
|
|
// interface implementation required by the OrbitClient
|
|
func (r *orbitPostSoftwareInstallResultRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
func (r *orbitPostSoftwareInstallResultRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
type orbitPostSoftwareInstallResultResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r orbitPostSoftwareInstallResultResponse) Error() error { return r.Err }
|
|
func (r orbitPostSoftwareInstallResultResponse) Status() int { return http.StatusNoContent }
|
|
|
|
func postOrbitSoftwareInstallResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*orbitPostSoftwareInstallResultRequest)
|
|
if err := svc.SaveHostSoftwareInstallResult(ctx, req.HostSoftwareInstallResultPayload); err != nil {
|
|
return orbitPostSoftwareInstallResultResponse{Err: err}, nil
|
|
}
|
|
return orbitPostSoftwareInstallResultResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) SaveHostSoftwareInstallResult(ctx context.Context, result *fleet.HostSoftwareInstallResultPayload) error {
|
|
// this is not a user-authenticated endpoint
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
return newOsqueryError("internal error: missing host from request context")
|
|
}
|
|
|
|
// always use the authenticated host's ID as host_id
|
|
result.HostID = host.ID
|
|
|
|
// If this is an intermediate failure that will be retried, handle it specially
|
|
if result.RetriesRemaining > 0 {
|
|
// Create a record while keeping the original pending
|
|
failedExecID, hsi, isNewRecord, err := svc.ds.CreateIntermediateInstallFailureRecord(ctx, result)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "save intermediate install failure")
|
|
}
|
|
|
|
// Only create an activity if this is a new record (not a replay of a previous request)
|
|
if isNewRecord {
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
nil,
|
|
fleet.ActivityTypeInstalledSoftware{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
SoftwareTitle: hsi.SoftwareTitle,
|
|
SoftwarePackage: hsi.SoftwarePackage,
|
|
InstallUUID: failedExecID,
|
|
Status: string(result.Status()),
|
|
Source: hsi.Source,
|
|
SelfService: hsi.SelfService,
|
|
PolicyID: nil,
|
|
PolicyName: nil,
|
|
FromSetupExperience: true, // We assume that retries only occur during setup experience
|
|
},
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "create activity for intermediate software installation failure")
|
|
}
|
|
}
|
|
|
|
// Don't update setup experience status for intermediate failures
|
|
return nil
|
|
}
|
|
|
|
var fromSetupExperience bool
|
|
if fleet.IsSetupExperienceSupported(host.Platform) {
|
|
// This might be a setup experience software install result, so we attempt to update the
|
|
// "Setup experience" status for that item.
|
|
hostUUID, err := fleet.HostUUIDForSetupExperience(host)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "failed to get host's UUID for the setup experience")
|
|
}
|
|
if updated, err := maybeUpdateSetupExperienceStatus(ctx, svc.ds, fleet.SetupExperienceSoftwareInstallResult{
|
|
HostUUID: hostUUID,
|
|
ExecutionID: result.InstallUUID,
|
|
InstallerStatus: result.Status(),
|
|
}, true); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "update setup experience status")
|
|
} else if updated {
|
|
level.Debug(svc.logger).Log(
|
|
"msg", "setup experience software install result updated",
|
|
"host_uuid", hostUUID,
|
|
"execution_id", result.InstallUUID,
|
|
)
|
|
fromSetupExperience = true
|
|
// We need to trigger the next step to properly support setup experience on Linux.
|
|
// On Linux, users can skip the setup experience by closing the "My device" page.
|
|
if _, err := svc.EnterpriseOverrides.SetupExperienceNextStep(ctx, host); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "getting next step for host setup experience")
|
|
}
|
|
}
|
|
}
|
|
|
|
installWasCanceled, err := svc.ds.SetHostSoftwareInstallResult(ctx, result)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "save host software installation result")
|
|
}
|
|
|
|
// do not create a "past" activity if the status is not terminal or if the activity
|
|
// was canceled.
|
|
if status := result.Status(); status != fleet.SoftwareInstallPending && !installWasCanceled {
|
|
hsi, err := svc.ds.GetSoftwareInstallResults(ctx, result.InstallUUID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get host software installation result information")
|
|
}
|
|
|
|
// Self-Service installs, and installs made by automations, will have a nil author for the activity.
|
|
var user *fleet.User
|
|
if !hsi.SelfService && hsi.UserID != nil {
|
|
user, err = svc.ds.UserByID(ctx, *hsi.UserID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get host software installation user")
|
|
}
|
|
}
|
|
|
|
var policyName *string
|
|
if hsi.PolicyID != nil {
|
|
if policy, err := svc.ds.PolicyLite(ctx, *hsi.PolicyID); err == nil && policy != nil {
|
|
policyName = &policy.Name // fall back to blank policy name if we can't retrieve the policy
|
|
}
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
user,
|
|
fleet.ActivityTypeInstalledSoftware{
|
|
HostID: host.ID,
|
|
HostDisplayName: host.DisplayName(),
|
|
SoftwareTitle: hsi.SoftwareTitle,
|
|
SoftwarePackage: hsi.SoftwarePackage,
|
|
InstallUUID: result.InstallUUID,
|
|
Status: string(status),
|
|
Source: hsi.Source,
|
|
SelfService: hsi.SelfService,
|
|
PolicyID: hsi.PolicyID,
|
|
PolicyName: policyName,
|
|
FromSetupExperience: fromSetupExperience,
|
|
},
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "create activity for software installation")
|
|
}
|
|
|
|
// lastly, queue a vitals refetch so we get a proper view of inventory from osquery
|
|
if status == fleet.SoftwareInstalled {
|
|
if err := svc.ds.UpdateHostRefetchRequested(ctx, host.ID, true); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "queue host vitals refetch")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Get Orbit setup experience status
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getOrbitSetupExperienceStatusRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
ForceRelease bool `json:"force_release"`
|
|
// Whether to re-enqueue canceled setup experience steps after a previous
|
|
// software install failure on MacOS.
|
|
ResetFailedSetupSteps bool `json:"reset_failed_setup_steps"`
|
|
}
|
|
|
|
func (r *getOrbitSetupExperienceStatusRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
func (r *getOrbitSetupExperienceStatusRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
type getOrbitSetupExperienceStatusResponse struct {
|
|
Results *fleet.SetupExperienceStatusPayload `json:"setup_experience_results,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getOrbitSetupExperienceStatusResponse) Error() error { return r.Err }
|
|
|
|
func getOrbitSetupExperienceStatusEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*getOrbitSetupExperienceStatusRequest)
|
|
results, err := svc.GetOrbitSetupExperienceStatus(ctx, req.OrbitNodeKey, req.ForceRelease, req.ResetFailedSetupSteps)
|
|
if err != nil {
|
|
return &getOrbitSetupExperienceStatusResponse{Err: err}, nil
|
|
}
|
|
return &getOrbitSetupExperienceStatusResponse{Results: results}, nil
|
|
}
|
|
|
|
func (svc *Service) GetOrbitSetupExperienceStatus(ctx context.Context, orbitNodeKey string, forceRelease bool, resetFailedSetupSteps bool) (*fleet.SetupExperienceStatusPayload, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Setup experience init
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type orbitSetupExperienceInitRequest struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
}
|
|
|
|
func (r *orbitSetupExperienceInitRequest) setOrbitNodeKey(nodeKey string) {
|
|
r.OrbitNodeKey = nodeKey
|
|
}
|
|
|
|
func (r *orbitSetupExperienceInitRequest) orbitHostNodeKey() string {
|
|
return r.OrbitNodeKey
|
|
}
|
|
|
|
type orbitSetupExperienceInitResponse struct {
|
|
Result fleet.SetupExperienceInitResult `json:"result"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r orbitSetupExperienceInitResponse) Error() error {
|
|
return r.Err
|
|
}
|
|
|
|
func orbitSetupExperienceInitEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
_, ok := request.(*orbitSetupExperienceInitRequest)
|
|
if !ok {
|
|
return nil, fmt.Errorf("internal error: invalid request type: %T", request)
|
|
}
|
|
result, err := svc.SetupExperienceInit(ctx)
|
|
if err != nil {
|
|
return orbitSetupExperienceInitResponse{Err: err}, nil
|
|
}
|
|
return orbitSetupExperienceInitResponse{
|
|
Result: *result,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) SetupExperienceInit(ctx context.Context) (*fleet.SetupExperienceInitResult, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|