You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Make templates real: the POST /api/v1/fleet/name_template endpoint (license + authz + validation + activity), team-payload/spec plumbing (which GitOps rides on), bulk-marking hosts on save, the cron step that resolves and enqueues Settings commands, the command-result handler (ACK → rename host in Fleet + verifying; Error → failed), and independent verification/drift detection at both name-ingestion sites. After this merges, saving a template renames a fleet end to end.
Endpoint (server/service/mdm.go, register in server/service/handler.go next to line 846)
license check first (license.FromContext → svc.authz.SkipAuthorization + fleet.ErrMissingLicense), then svc.authz.Authorize(ctx, fleet.MDMAppleSettingsPayload{TeamID: teamID}, fleet.ActionWrite) — same authz object as disk encryption (story: "Same permissions as recovery lock, disk encryption").
teamID == nil → 422 "name_template requires fleet_id; this setting is only available for fleets" (fleets-only decision).
non-empty → svc.ds.BulkUpsertHostDeviceNameEnforcement(ctx, tm.ID); empty → svc.ds.DeleteHostDeviceNameEnforcementForTeam(ctx, tm.ID) (clearing never renames — rows are deleted, no command sent).
Also plumb TeamPayloadMDM.NameTemplate through ModifyTeam (set near ee/server/service/teams.go:313-319, activity near 604-614) and TeamSpecMDM.NameTemplate through team-spec apply (teams.go:1580, 1765, 2028 regions), all funneling into the same helper so PATCH /teams/{id}, spec apply, GitOps, and the dedicated endpoint behave identically (validation + activity + row reconcile).
Cron enqueue step
Add a step to the mdm_apple_profile_manager cron (registered at cmd/fleet/cron_registration.go:224; symbol fleet.CronMDMAppleProfileManager, cmd/fleet/cron.go:1888; its main loop calls ReconcileAppleProfilesBatched at cron.go:1900 — add the device-name step alongside):
per host: resolved := fleet.ResolveHostNameTemplate(team.Config.MDM.NameTemplate, host); enforce a 63-byte UTF-8 cap on the resolved name — if exceeded, set row failed with detail Resolved name exceeds 63 bytes. (Apple truncates/rejects longer names — SME confirm exact limit);
skip if the host's current computer_name already equals resolved → mark row verified directly (no needless command);
Route by UUID prefix before the requestType switch, like the refetch prefix routing at apple_mdm.go:4167-4171 (why prefix, not a "Settings" case: future features may send other Settings items; the prefix scopes handling to renames):
Acknowledged: fetch row by command_uuid; update hosts.computer_name + hostname to expected_device_name and upsert host_display_names (see writes at server/datastore/mysql/hosts.go:2427, display-name derivation server/fleet/hosts.go:1008); set row verifying. Parse per-item status from the Settings ack payload if present (each Settings item can individually error — SME confirm shape) → treat item-level error as Error.
Error/CommandFormatError: row failed, detail = Apple error chain (format like the InstallProfile case at apple_mdm.go:4182).
Map via mdmAppleDeliveryStatusFromCommandStatus (apple_mdm.go:5504) where convenient.
iOS/iPadOS: in handleRefetchDeviceResults (apple_mdm.go:5275), after DeviceName is extracted (apple_mdm.go:5296, 5337-5340), call ds.VerifyHostDeviceName(ctx, host.UUID, deviceName).
macOS: osquery system_info ingestion sets host.ComputerName in-memory (server/service/osquery_utils/queries.go:381-420, computer_name at :418) without DB access; hook the comparison where detail results are persisted in the osquery service flow (after the ingest funcs run and the host is saved) — call ds.VerifyHostDeviceName(ctx, host.UUID, host.ComputerName) only when a rename row exists (cheap indexed lookup; consider piggybacking on an existing per-detail-ingest DB call to avoid a hot-path query for hosts with no enforcement).
VerifyHostDeviceName semantics (from sub-issue Rename hosts: enforcement-state migrations and datastore methods #48620): rows in verifying/verified only; match → verified; mismatch → failed with detail Host was renamed on the device and no longer matches the fleet's naming template. (drift → Failed; this is the piece the Figma dev note marks as cuttable under capacity pressure).
Condition of satisfaction
Endpoint:MYSQL_TEST=1 REDIS_TEST=1 go test ./server/service/... — 402/ErrMissingLicense on free tier; 401/403 for observer & team-scoped users of other teams (same matrix as disk encryption); 422 for missing fleet_id, bad variable, $FLEET_SECRET_*, control chars; 204 + edited_host_name_template activity (with name_template: null when cleared) on success; saving the identical template emits no duplicate activity and re-enqueues nothing.
Rows on save: saving a template creates NULL-status rows for eligible hosts only; clearing deletes all rows for the team without touching hosts.computer_name.
Cron: enterprise integration test — pending row + eligible host → command enqueued with DEVNAME- prefix and resolved name (serial expanded); already-matching host goes straight to verified; BYOD host has no row and gets nothing; >63-byte resolution goes failed; a simulated unsupervised-device command Error lands failed with Apple's error in detail and is not re-sent by subsequent cron runs.
Ack/Error: simulated ACK renames the host in Fleet (computer_name, hostname, display name) and sets verifying; simulated Error sets failed with the Apple error chain in detail.
Verification: reported name == expected flips verifying→verified; a later mismatched report flips verified→failed (drift), for both the iOS refetch path and the macOS detail-ingest path; hosts with no row are untouched (no hot-path regression — add a benchmark-ish assertion or at minimum verify no extra query when no enforcement exists).
End-to-end (integration-mdm bundle): set template → cron → ack → refetch-verify → verified, on a fake MDM device.
Related user story
#38806
Task
Make templates real: the
POST /api/v1/fleet/name_templateendpoint (license + authz + validation + activity), team-payload/spec plumbing (which GitOps rides on), bulk-marking hosts on save, the cron step that resolves and enqueuesSettingscommands, the command-result handler (ACK → rename host in Fleet +verifying; Error →failed), and independent verification/drift detection at both name-ingestion sites. After this merges, saving a template renames a fleet end to end.Endpoint (
server/service/mdm.go, register inserver/service/handler.gonext to line 846)Follow
updateDiskEncryptionEndpoint(mdm.go:2986-3034) exactly:Core
Service.UpdateMDMNameTemplate(ctx, teamID *uint, nameTemplate string) error:license.FromContext→svc.authz.SkipAuthorization+fleet.ErrMissingLicense), thensvc.authz.Authorize(ctx, fleet.MDMAppleSettingsPayload{TeamID: teamID}, fleet.ActionWrite)— same authz object as disk encryption (story: "Same permissions as recovery lock, disk encryption").teamID == nil→ 422 "name_template requires fleet_id; this setting is only available for fleets" (fleets-only decision).fleet.ValidateHostNameTemplate(sub-issue Rename hosts: config types, template validation, and activity type #48621) → 422 with the validator's message on failure.svc.EnterpriseOverrides.UpdateTeamMDMNameTemplate(ctx, tm, nameTemplate); wire the override inee/server/service/service.go(like line 96).EE implementation (
ee/server/service/teams.go, followupdateTeamMDMDiskEncryptionatteams.go:2218-2275)updateTeamMDMNameTemplate(ctx, tm *fleet.Team, nameTemplate string):tm.Config.MDM.NameTemplate == nameTemplate(idempotent; avoids re-enqueue storms on repeated saves — mirrors "exclude current entity" upsert guidance);svc.ds.SaveTeam;ActivityTypeEditedHostNameTemplate{TeamID, TeamName, NameTemplate: nilIfEmpty};svc.ds.BulkUpsertHostDeviceNameEnforcement(ctx, tm.ID); empty →svc.ds.DeleteHostDeviceNameEnforcementForTeam(ctx, tm.ID)(clearing never renames — rows are deleted, no command sent).Also plumb
TeamPayloadMDM.NameTemplatethroughModifyTeam(set nearee/server/service/teams.go:313-319, activity near604-614) andTeamSpecMDM.NameTemplatethrough team-spec apply (teams.go:1580, 1765, 2028regions), all funneling into the same helper soPATCH /teams/{id}, spec apply, GitOps, and the dedicated endpoint behave identically (validation + activity + row reconcile).Cron enqueue step
Add a step to the
mdm_apple_profile_managercron (registered atcmd/fleet/cron_registration.go:224; symbolfleet.CronMDMAppleProfileManager,cmd/fleet/cron.go:1888; its main loop callsReconcileAppleProfilesBatchedatcron.go:1900— add the device-name step alongside):ListHostsPendingDeviceNameCommand(ctx, batchLimit);resolved := fleet.ResolveHostNameTemplate(team.Config.MDM.NameTemplate, host); enforce a 63-byte UTF-8 cap on the resolved name — if exceeded, set rowfailedwith detailResolved name exceeds 63 bytes.(Apple truncates/rejects longer names — SME confirm exact limit);computer_namealready equalsresolved→ mark rowverifieddirectly (no needless command);cmdUUID := fleet.DeviceNameCommandUUIDPrefix + uuid.New().String();commander.DeviceNameSetting(...);SetHostDeviceNameCommandSent(...).Command results handler (
server/service/apple_mdm.go)Route by UUID prefix before the requestType switch, like the refetch prefix routing at
apple_mdm.go:4167-4171(why prefix, not a"Settings"case: future features may send otherSettingsitems; the prefix scopes handling to renames):Acknowledged: fetch row bycommand_uuid; updatehosts.computer_name+hostnametoexpected_device_nameand upserthost_display_names(see writes atserver/datastore/mysql/hosts.go:2427, display-name derivationserver/fleet/hosts.go:1008); set rowverifying. Parse per-item status from theSettingsack payload if present (each Settings item can individually error — SME confirm shape) → treat item-level error asError.Error/CommandFormatError: rowfailed,detail= Apple error chain (format like the InstallProfile case atapple_mdm.go:4182).mdmAppleDeliveryStatusFromCommandStatus(apple_mdm.go:5504) where convenient.Verification + drift detection (both name-ingestion sites)
handleRefetchDeviceResults(apple_mdm.go:5275), afterDeviceNameis extracted (apple_mdm.go:5296, 5337-5340), callds.VerifyHostDeviceName(ctx, host.UUID, deviceName).system_infoingestion setshost.ComputerNamein-memory (server/service/osquery_utils/queries.go:381-420, computer_name at:418) without DB access; hook the comparison where detail results are persisted in the osquery service flow (after the ingest funcs run and the host is saved) — callds.VerifyHostDeviceName(ctx, host.UUID, host.ComputerName)only when a rename row exists (cheap indexed lookup; consider piggybacking on an existing per-detail-ingest DB call to avoid a hot-path query for hosts with no enforcement).VerifyHostDeviceNamesemantics (from sub-issue Rename hosts: enforcement-state migrations and datastore methods #48620): rows inverifying/verifiedonly; match →verified; mismatch →failedwith detailHost was renamed on the device and no longer matches the fleet's naming template.(drift → Failed; this is the piece the Figma dev note marks as cuttable under capacity pressure).Condition of satisfaction
MYSQL_TEST=1 REDIS_TEST=1 go test ./server/service/...— 402/ErrMissingLicenseon free tier; 401/403 for observer & team-scoped users of other teams (same matrix as disk encryption); 422 for missing fleet_id, bad variable,$FLEET_SECRET_*, control chars; 204 +edited_host_name_templateactivity (withname_template: nullwhen cleared) on success; saving the identical template emits no duplicate activity and re-enqueues nothing.hosts.computer_name.DEVNAME-prefix and resolved name (serial expanded); already-matching host goes straight toverified; BYOD host has no row and gets nothing; >63-byte resolution goesfailed; a simulated unsupervised-device command Error landsfailedwith Apple's error indetailand is not re-sent by subsequent cron runs.verifying; simulated Error setsfailedwith the Apple error chain indetail.verifying→verified; a later mismatched report flipsverified→failed(drift), for both the iOS refetch path and the macOS detail-ingest path; hosts with no row are untouched (no hot-path regression — add a benchmark-ish assertion or at minimum verify no extra query when no enforcement exists).verified, on a fake MDM device.