Files
fleet/server/service/script_variables.go
T
Jordan Montgomery 89f67544b4 Add support for fleet vars in scripts(controls scripts, software scripts/script-only packages and setup experience scripts) (#49781)
<!-- 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 -->
2026-07-27 11:24:42 +02:00

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
}