Demo: https://www.youtube.com/watch?v=cWxZlu9WuwA Guide updates: https://github.com/fleetdm/fleet/pull/49603/changes IT admins can configure the fleet that hosts enrolling through user-driven Windows MDM enrollment (Windows Autopilot, Entra join) are automatically assigned to, via the Windows MDM settings page, the mdm.windows_enrollment.default_fleet config setting, or GitOps. - New windows_enrollment_config row stores the default team; the config API surfaces it by fleet name and hydrates reads from the row so team renames and deletions never serve a stale name. Deleting the fleet clears the setting. - New edited_windows_enrollment_default_fleet activity, emitted only when the value changes. - The OMA-DM session persists the device-reported SMBIOS serial on still-unlinked enrollments, and orbit enrollment reverse-links by that serial and assigns the default fleet before orbit's one-shot setup-experience init, so the default fleet's software, scripts, and profiles apply during the Autopilot ESP. The DevDetail and osquery link paths keep the same assignment as fallbacks, and the EUA-token link path now shares the same post-link bookkeeping. - Hosts are only assigned when new to Fleet in this enrollment cycle: existing hosts, including ones parked in Unassigned, keep their fleet on re-enrollment, matching macOS ABM behavior. - GitOps defers applying the setting until teams declared in the same run are created, and fleetctl generate-gitops exports it. - Windows MDM settings page redesign per Figma: programmatic enrollment toggle, User driven enrollment section with the Entra-gated Default fleet dropdown, and a Migration section. <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #41787 # 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 ## 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 ## Database migrations - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). ## New Fleet configuration settings - [x] Verified that the setting is exported via `fleetctl generate-gitops` - [x] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - [x] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [x] Verified that any relevant UI is disabled when GitOps mode is enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for assigning a default Fleet Premium fleet to new Windows MDM enrollments, including Autopilot and Entra join. * Default-fleet settings can be configured, cleared, and managed through Windows MDM settings and GitOps. * Assigned fleet software, scripts, and profiles can apply during out-of-box setup. * Added activity-feed visibility for default-fleet changes. * Improved Windows enrollment matching using hardware serial numbers. * **Documentation** * Documented default-fleet assignment for Windows enrollment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
264 lines
7.6 KiB
Go
264 lines
7.6 KiB
Go
// Command cloner-check is used to verify that each item stored in the
|
|
// cached_mysql in-memory cache properly implements the fleet.Cloner interface.
|
|
//
|
|
// There are two ways to use this command, the first one is typically used by
|
|
// CI and checks that the cacheable items don't have any changes when compared
|
|
// to the current generated files. That is, if a field is added or modified in
|
|
// a cacheable struct, and those changes haven't been reflected in the
|
|
// generated files yet, it will raise an error, ensuring that the developer
|
|
// takes those changes into account in the custom Clone implementation.
|
|
//
|
|
// The --check flag runs this check mode scenario, but it is optional as running
|
|
// in check mode is the default:
|
|
//
|
|
// $ go run ./tools/cloner-check/main.go [--check]
|
|
//
|
|
// (or alternatively "make check-go-cloner")
|
|
//
|
|
// The second way to use this command is with the --update flag, which is used
|
|
// to update the generated files with the current version of the cacheable
|
|
// items (i.e. the current struct definition). Use this when you've
|
|
// double-checked that the custom Clone implementation is up-to-date and
|
|
// correct.
|
|
//
|
|
// $ go run ./tools/cloner-check/main.go --update
|
|
//
|
|
// (or atternatively "make update-go-cloner")
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/pmezard/go-difflib/difflib"
|
|
)
|
|
|
|
// If you add a new cacheable struct, make sure to add it to this list.
|
|
var cacheableItems = []fleet.Cloner{
|
|
&fleet.AppConfig{},
|
|
&fleet.Pack{},
|
|
&fleet.ScheduledQuery{},
|
|
&fleet.Features{},
|
|
&fleet.TeamMDM{},
|
|
&fleet.TeamConfig{},
|
|
&fleet.Query{},
|
|
&fleet.MDMProfileSpec{},
|
|
&fleet.MDMConfigAsset{},
|
|
&fleet.YaraRule{},
|
|
&fleet.WindowsEnrollmentDefaultFleet{},
|
|
// TeamAgentOptions is not in the list because it is a json.RawMessage, no fields can change.
|
|
// Same for ResultCountForQuery, it's just an int.
|
|
}
|
|
|
|
func main() {
|
|
flagCheck := flag.Bool("check", false, "Run in check mode (default if no flag is provided)")
|
|
flagUpdate := flag.Bool("update", false, "Update the generated files with the current cacheable items")
|
|
flag.Parse()
|
|
|
|
// make sure this is run from the root of the repository
|
|
if _, err := os.Stat(filepath.Join("tools", "cloner-check", "main.go")); os.IsNotExist(err) {
|
|
fmt.Fprintf(os.Stderr, "error: this command must be run from the root of the repository\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if *flagUpdate {
|
|
updateCacheableItems()
|
|
return
|
|
}
|
|
|
|
if *flagCheck || !*flagUpdate {
|
|
if !checkCacheableItems() {
|
|
fmt.Fprintf(os.Stderr, `
|
|
Some cacheable items failed the check, ensure you do the following:
|
|
|
|
1. Verify the Cloner implementation for that type, make sure it takes the new/updated field(s) into account if necessary.
|
|
2. Run "go run ./tools/cloner-check/main.go --update" (or "make update-go-cloner") to update the generated files and fix this check.
|
|
`)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkCacheableItems() bool {
|
|
ok := true
|
|
|
|
for _, item := range cacheableItems {
|
|
itemType, _ := getUnderlyingStructType(reflect.TypeOf(item))
|
|
filename := typeToFilename(itemType)
|
|
|
|
want, err := os.ReadFile(filepath.Join("tools", "cloner-check", "generated_files", filename))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: error reading file %s: %v\n", itemType, filename, err)
|
|
ok = false
|
|
continue
|
|
}
|
|
|
|
var sb strings.Builder
|
|
if err := generateFieldsList(&sb, item); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: error generating field list: %v\n", itemType, err)
|
|
ok = false
|
|
continue
|
|
}
|
|
|
|
diff := difflib.UnifiedDiff{
|
|
A: difflib.SplitLines(string(want)),
|
|
B: difflib.SplitLines(sb.String()),
|
|
FromFile: filename,
|
|
ToFile: "current",
|
|
Context: 2,
|
|
}
|
|
text, err := difflib.GetUnifiedDiffString(diff)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(text) != 0 {
|
|
fmt.Fprintf(os.Stderr, "%s: fields mismatch vs file %s:\n%s", itemType, filename, text)
|
|
ok = false
|
|
continue
|
|
}
|
|
}
|
|
|
|
return ok
|
|
}
|
|
|
|
func updateCacheableItems() bool {
|
|
ok := true
|
|
|
|
for _, item := range cacheableItems {
|
|
itemType, _ := getUnderlyingStructType(reflect.TypeOf(item))
|
|
filename := typeToFilename(itemType)
|
|
|
|
var sb strings.Builder
|
|
if err := generateFieldsList(&sb, item); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: error generating field list: %v\n", itemType, err)
|
|
ok = false
|
|
continue
|
|
}
|
|
if err := os.WriteFile(filepath.Join("tools", "cloner-check", "generated_files", filename), []byte(sb.String()), 0644); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: error generating file %s: %v\n", itemType, filename, err)
|
|
ok = false
|
|
continue
|
|
}
|
|
}
|
|
|
|
return ok
|
|
}
|
|
|
|
func typeToFilename(t reflect.Type) string {
|
|
return fmt.Sprintf("%s.txt", strings.ToLower(t.Name()))
|
|
}
|
|
|
|
func generateFieldsList(w io.Writer, item fleet.Cloner) error {
|
|
// keep a map of already-printed types, to avoid printing the same type multiple times
|
|
seenTypes := make(map[string]bool)
|
|
t := reflect.TypeOf(item)
|
|
return generateStructFieldsList(w, t, seenTypes)
|
|
}
|
|
|
|
var basicTypes = map[reflect.Kind]bool{
|
|
reflect.Bool: true,
|
|
reflect.Int: true,
|
|
reflect.Int8: true,
|
|
reflect.Int16: true,
|
|
reflect.Int32: true,
|
|
reflect.Int64: true,
|
|
reflect.Uint: true,
|
|
reflect.Uint8: true,
|
|
reflect.Uint16: true,
|
|
reflect.Uint32: true,
|
|
reflect.Uint64: true,
|
|
reflect.Uintptr: true,
|
|
reflect.Float32: true,
|
|
reflect.Float64: true,
|
|
reflect.Complex64: true,
|
|
reflect.Complex128: true,
|
|
reflect.String: true,
|
|
}
|
|
|
|
func generateStructFieldsList(w io.Writer, t reflect.Type, seenTypes map[string]bool) error {
|
|
for t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
|
|
// should always be a struct in the end
|
|
if t.Kind() != reflect.Struct {
|
|
panic("generateStructFieldsList called with non-struct type: " + t.String())
|
|
}
|
|
|
|
key := fmt.Sprintf("%s/%s", t.PkgPath(), t.Name())
|
|
|
|
if seenTypes[key] {
|
|
return nil
|
|
}
|
|
seenTypes[key] = true
|
|
|
|
count := t.NumField()
|
|
for i := 0; i < count; i++ {
|
|
field := t.Field(i)
|
|
|
|
// if the type is defined as a basic type, add that information to the line.
|
|
if basicTypes[field.Type.Kind()] && field.Type.PkgPath() != "" {
|
|
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", key, field.Name, field.Type.String(), field.Type.Kind()); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\n", key, field.Name, field.Type.String()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// if the field is (eventually) a struct, print this struct's fields too
|
|
// (this resolves pointers to structs, slices, arrays, and maps, which is
|
|
// why it receives potentially 2 types - map key and value)
|
|
st1, st2 := getUnderlyingStructType(field.Type)
|
|
if st1 != nil {
|
|
if err := generateStructFieldsList(w, st1, seenTypes); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if st2 != nil {
|
|
if err := generateStructFieldsList(w, st2, seenTypes); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getUnderlyingStructType(t reflect.Type) (st1, st2 reflect.Type) {
|
|
for {
|
|
if t.Kind() == reflect.Pointer || t.Kind() == reflect.Slice || t.Kind() == reflect.Array {
|
|
t = t.Elem()
|
|
continue
|
|
}
|
|
|
|
if t.Kind() == reflect.Map {
|
|
// a map's key cannot be a map, so we can safely call
|
|
// getUnderlyingStructType again on the map's key type
|
|
st1, _ = getUnderlyingStructType(t.Key())
|
|
// and then do the same for the map's value type
|
|
k, v := getUnderlyingStructType(t.Elem())
|
|
// however, this does not support a map of maps, so if the value has two types (was a map),
|
|
// we panic.
|
|
if k != nil && v != nil {
|
|
panic("unsupported map of maps: " + t.String())
|
|
}
|
|
st2 = k
|
|
return st1, st2
|
|
}
|
|
|
|
if t.Kind() == reflect.Struct {
|
|
return t, nil
|
|
}
|
|
|
|
// not a pointer, slice, array, map nor struct
|
|
return nil, nil
|
|
}
|
|
}
|