<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #49511 and #46837 as a whole # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for Fleet built-in variables in host scripts, software installer scripts, setup-experience scripts, and maintained-app installer scripts. * Variables are resolved per host at execution time; saved content remains unexpanded. * **Bug Fixes** * Requests now validate Fleet variables up-front, with clear script-specific error messages for unsupported variables. * Added improved messaging when variable resolution fails during execution. * Enforced Fleet Premium licensing for script/installer flows that use Fleet variables. * **Documentation** * Documented supported variables and Premium requirements, including usage examples. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
97 lines
3.4 KiB
Go
97 lines
3.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/profiles"
|
|
"github.com/fleetdm/fleet/v4/server/variables"
|
|
)
|
|
|
|
// maybeExpandScriptFleetVariables resolves supported $FLEET_VAR_* references
|
|
// in contents for the given host. It returns the expanded contents, or a
|
|
// non-empty failureMessage when a variable exists but can't be resolved for
|
|
// this host (one line per failing variable). Unsupported variable names are
|
|
// left untouched: validation rejects them in new content, and content saved
|
|
// before validation shipped must keep working unchanged. Known limit of
|
|
// variables.Replace, accepted because validation rejects unsupported names
|
|
// going forward: in pre-validation content, an unsupported name that extends
|
|
// a supported one (e.g. $FLEET_VAR_HOST_UUID_SUFFIX) has its prefix replaced
|
|
// along with the supported variable. Supported names that extend each other
|
|
// (e.g. ..._IDP_USERNAME and ..._IDP_USERNAME_LOCAL_PART) are safe because
|
|
// variables.Find returns names longest-first and each is replaced in turn.
|
|
func (svc *Service) maybeExpandScriptFleetVariables(ctx context.Context, host *fleet.Host, contents string) (expanded string, failureMessage string, err error) {
|
|
fleetVars := variables.Find(contents)
|
|
if len(fleetVars) == 0 {
|
|
return contents, "", nil
|
|
}
|
|
|
|
// defensive re-check in case variable-bearing content slipped past upload
|
|
// validation (e.g. saved before validation shipped, or the license expired)
|
|
if !license.IsPremium(ctx) {
|
|
return "", "Fleet couldn't run this script because it uses variables, which require a Fleet Premium license.", nil
|
|
}
|
|
|
|
// collect all failures instead of stopping at the first one so the admin
|
|
// can fix everything in one pass
|
|
var failures []string
|
|
fail := func(errMsg string) error {
|
|
failures = append(failures, errMsg)
|
|
return nil
|
|
}
|
|
|
|
hostIDForUUIDCache := map[string]uint{host.UUID: host.ID}
|
|
for _, v := range fleetVars {
|
|
if !slices.Contains(fleet.FleetVarsSupportedInScripts, fleet.FleetVarName(v)) {
|
|
continue
|
|
}
|
|
|
|
var value string
|
|
switch fleet.FleetVarName(v) {
|
|
case fleet.FleetVarHostUUID:
|
|
value = host.UUID
|
|
if value == "" {
|
|
_ = fail(fmt.Sprintf("There is no UUID for this host. Fleet couldn't populate $FLEET_VAR_%s.", v))
|
|
continue
|
|
}
|
|
case fleet.FleetVarHostHardwareSerial:
|
|
value = host.HardwareSerial
|
|
if value == "" {
|
|
_ = fail(fmt.Sprintf("There is no hardware serial for this host. Fleet couldn't populate $FLEET_VAR_%s.", v))
|
|
continue
|
|
}
|
|
case fleet.FleetVarHostPlatform:
|
|
value = host.Platform
|
|
if value == "darwin" {
|
|
value = "macos"
|
|
}
|
|
if value == "" {
|
|
_ = fail(fmt.Sprintf("There is no platform for this host. Fleet couldn't populate $FLEET_VAR_%s.", v))
|
|
continue
|
|
}
|
|
default: // the IdP variables
|
|
idpValue, _, ok, err := profiles.ResolveHostEndUserIDPValue(ctx, svc.ds, v, host.UUID, hostIDForUUIDCache, fail)
|
|
if err != nil {
|
|
return "", "", ctxerr.Wrap(ctx, err, "resolve IdP variable for script")
|
|
}
|
|
if !ok {
|
|
// the fail callback recorded the reason
|
|
continue
|
|
}
|
|
value = idpValue
|
|
}
|
|
|
|
contents = variables.Replace(contents, v, value)
|
|
}
|
|
|
|
if len(failures) > 0 {
|
|
return "", strings.Join(failures, "\n"), nil
|
|
}
|
|
return contents, "", nil
|
|
}
|