From 04f9f7b4831618f70c665fe1b042b4366a5652fb Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Mon, 1 Jun 2026 11:34:25 -0400 Subject: [PATCH 1/7] Initial rename --- changes/42508-rename-abm-to-ab | 1 + cmd/fleetctl/fleetctl/generate.go | 105 +++++++++++------- cmd/fleetctl/fleetctl/generate_gitops_test.go | 2 +- cmd/fleetctl/fleetctl/get.go | 103 ++++++++++------- cmd/fleetctl/fleetctl/get_test.go | 6 +- cmd/fleetctl/fleetctl/gitops.go | 15 ++- cmd/fleetctl/fleetctl/gitops_test.go | 4 +- .../templates/new/default.template.yml | 2 +- .../expectedGetConfigAppConfigJson.json | 1 + ...dGetConfigAppConfigTeamMaintainerJson.json | 1 + ...edGetConfigAppConfigTeamMaintainerYaml.yml | 1 + .../expectedGetConfigAppConfigYaml.yml | 1 + ...ectedGetConfigIncludeServerConfigJson.json | 1 + ...pectedGetConfigIncludeServerConfigYaml.yml | 1 + .../expectedOrgSettings-insecure.yaml | 2 +- .../generateGitops/expectedOrgSettings.yaml | 2 +- .../test_dir_premium/default.yml | 2 +- .../macosSetupExpectedAppConfigEmpty.yml | 1 + .../macosSetupExpectedAppConfigSet.yml | 1 + ee/server/service/mdm.go | 2 +- frontend/components/App/App.tsx | 16 +-- frontend/interfaces/mdm.ts | 2 +- .../AppleBusinessManagerPage.tsx | 28 ++--- .../AppleBusinessManagerTable.tsx | 18 +-- .../AppleBusinessManagerTableConfig.tsx | 14 +-- .../EditTeamsAbmModal/EditTeamsAbmModal.tsx | 10 +- .../components/VppTable/VppTableConfig.tsx | 4 +- frontend/services/entities/mdm_apple_bm.ts | 38 +++---- frontend/utilities/endpoints.ts | 12 +- pkg/spec/gitops_deprecations.go | 15 ++- pkg/spec/gitops_test.go | 73 ++++++++++++ server/fleet/app.go | 7 +- server/fleet/errors.go | 2 +- server/service/appconfig.go | 6 +- server/service/apple_mdm.go | 8 +- server/service/client.go | 4 +- server/service/client_mdm.go | 2 +- server/service/handler.go | 14 +-- server/service/handler_deprecated_paths.go | 30 ++++- 39 files changed, 360 insertions(+), 197 deletions(-) create mode 100644 changes/42508-rename-abm-to-ab diff --git a/changes/42508-rename-abm-to-ab b/changes/42508-rename-abm-to-ab new file mode 100644 index 00000000000..b89d47524e6 --- /dev/null +++ b/changes/42508-rename-abm-to-ab @@ -0,0 +1 @@ +- Renamed Apple Business Manager (ABM) terminology to Apple Business (AB) in the API, GitOps YAML, and `fleetctl` CLI. The new `/api/v1/fleet/ab_tokens` endpoints, `mdm.apple_business` YAML key, and `fleetctl get mdm-ab`/`fleetctl generate mdm-ab` commands are canonical however the now-deprecated `/abm_tokens`, `apple_business_manager`, `mdm-apple-bm` aliases continue to work for backwards compatibility and log a deprecation warning when used. diff --git a/cmd/fleetctl/fleetctl/generate.go b/cmd/fleetctl/fleetctl/generate.go index 5456d4810df..cef15fa33f7 100644 --- a/cmd/fleetctl/fleetctl/generate.go +++ b/cmd/fleetctl/fleetctl/generate.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/fleetdm/fleet/v4/server/platform/logging" "github.com/urfave/cli/v2" ) @@ -23,6 +24,7 @@ func generateCommand() *cli.Command { }, Subcommands: []*cli.Command{ generateMDMAppleCommand(), + generateMDMABCommand(), generateMDMAppleBMCommand(), }, } @@ -87,62 +89,83 @@ Go to %s/settings/integrations/mdm/apple and follow the steps. } } +func generateMDMABCommand() *cli.Command { + return &cli.Command{ + Name: "mdm-ab", + Aliases: []string{"mdm_ab"}, + Usage: "Generate Apple Business (AB) public key to enable automatic enrollment for macOS hosts.", + Flags: generateMDMABFlags(), + Action: runGenerateMDMAB, + } +} + func generateMDMAppleBMCommand() *cli.Command { return &cli.Command{ Name: "mdm-apple-bm", Aliases: []string{"mdm_apple_bm"}, - Usage: "Generate Apple Business public key to enable automatic enrollment for macOS hosts.", - Flags: []cli.Flag{ - contextFlag(), - debugFlag(), - &cli.StringFlag{ - Name: "public-key", - Usage: "The output path for the Apple Business public key certificate.", - Value: bmPublicKeyCertPath, - }, - }, + Usage: "Deprecated. Use mdm-ab instead.", + Flags: generateMDMABFlags(), Action: func(c *cli.Context) error { - publicKeyPath := c.String("public-key") - - // get the fleet API client first, so that any login requirement are met - // before printing the CSR output message. - client, err := clientFromCLI(c) - if err != nil { - fmt.Fprintf(c.App.ErrWriter, "client from CLI: %s", err) - return ErrGeneric + if logging.TopicEnabled(logging.DeprecatedFieldTopic) { + fmt.Fprintf(c.App.ErrWriter, "[!] 'fleetctl generate mdm-apple-bm' is deprecated; use 'fleetctl generate mdm-ab' instead\n") } + return runGenerateMDMAB(c) + }, + } +} - publicKey, err := client.RequestAppleABM() - if err != nil { - fmt.Fprintf(c.App.ErrWriter, "requesting ABM public key: %s", err) - return ErrGeneric - } +func generateMDMABFlags() []cli.Flag { + return []cli.Flag{ + contextFlag(), + debugFlag(), + &cli.StringFlag{ + Name: "public-key", + Usage: "The output path for the Apple Business (AB) public key certificate.", + Value: bmPublicKeyCertPath, + }, + } +} - if err := os.WriteFile(publicKeyPath, publicKey, defaultFileMode); err != nil { - fmt.Fprintf(c.App.ErrWriter, "write public key: %s", err) - return ErrGeneric - } +func runGenerateMDMAB(c *cli.Context) error { + publicKeyPath := c.String("public-key") - appCfg, err := client.GetAppConfig() - if err != nil { - fmt.Fprintf(c.App.ErrWriter, "fetching app config: %s", err) - return ErrGeneric - } + // get the fleet API client first, so that any login requirement are met + // before printing the CSR output message. + client, err := clientFromCLI(c) + if err != nil { + fmt.Fprintf(c.App.ErrWriter, "client from CLI: %s", err) + return ErrGeneric + } - fmt.Fprintf( - c.App.Writer, - `Success! + publicKey, err := client.RequestAppleABM() + if err != nil { + fmt.Fprintf(c.App.ErrWriter, "requesting Apple Business public key: %s", err) + return ErrGeneric + } + + if err := os.WriteFile(publicKeyPath, publicKey, defaultFileMode); err != nil { + fmt.Fprintf(c.App.ErrWriter, "write public key: %s", err) + return ErrGeneric + } + + appCfg, err := client.GetAppConfig() + if err != nil { + fmt.Fprintf(c.App.ErrWriter, "fetching app config: %s", err) + return ErrGeneric + } + + fmt.Fprintf( + c.App.Writer, + `Success! Generated your public key at %s Go to %s/settings/integrations/automatic-enrollment/apple and follow the steps. `, - publicKeyPath, - appCfg.ServerSettings.ServerURL, - ) + publicKeyPath, + appCfg.ServerSettings.ServerURL, + ) - return nil - }, - } + return nil } diff --git a/cmd/fleetctl/fleetctl/generate_gitops_test.go b/cmd/fleetctl/fleetctl/generate_gitops_test.go index 6868e54e101..8151f464d1a 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops_test.go +++ b/cmd/fleetctl/fleetctl/generate_gitops_test.go @@ -2141,7 +2141,7 @@ func TestGenerateControlsAndMDMWithoutMDMEnabledAndConfigured(t *testing.T) { require.NoError(t, err) // Verify all keys are set to empty. for _, key := range []string{ - "apple_business_manager", + "apple_business", "apple_server_url", "end_user_authentication", "end_user_license_agreement", diff --git a/cmd/fleetctl/fleetctl/get.go b/cmd/fleetctl/fleetctl/get.go index 2fd57abe8fb..06beb6bf5b3 100644 --- a/cmd/fleetctl/fleetctl/get.go +++ b/cmd/fleetctl/fleetctl/get.go @@ -18,6 +18,7 @@ import ( "github.com/fleetdm/fleet/v4/pkg/rawjson" "github.com/fleetdm/fleet/v4/pkg/secure" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/platform/logging" "github.com/fleetdm/fleet/v4/server/service" "github.com/ghodss/yaml" kithttp "github.com/go-kit/kit/transport/http" @@ -334,6 +335,7 @@ func getCommand() *cli.Command { getFleetsCommand(), getSoftwareCommand(), getMDMAppleCommand(), + getMDMABCommand(), getMDMAppleBMCommand(), getMDMCommandResultsCommand(), getMDMCommandsCommand(), @@ -1473,58 +1475,79 @@ func getMDMAppleCommand() *cli.Command { } } +func getMDMABCommand() *cli.Command { + return &cli.Command{ + Name: "mdm-ab", + Aliases: []string{"mdm_ab"}, + Usage: "Show information about Apple Business (AB) for automatic enrollment", + Flags: getMDMABFlags(), + Action: runGetMDMAB, + } +} + func getMDMAppleBMCommand() *cli.Command { return &cli.Command{ Name: "mdm-apple-bm", Aliases: []string{"mdm_apple_bm"}, - Usage: "Show information about Apple Business for automatic enrollment", - Flags: []cli.Flag{ - configFlag(), - contextFlag(), - debugFlag(), - }, + Usage: "Deprecated. Use mdm-ab instead.", + Flags: getMDMABFlags(), Action: func(c *cli.Context) error { - const expirationWarning = 30 * 24 * time.Hour // 30 days - - client, err := clientFromCLI(c) - if err != nil { - return err + if logging.TopicEnabled(logging.DeprecatedFieldTopic) { + fmt.Fprintf(c.App.ErrWriter, "[!] 'fleetctl get mdm-apple-bm' is deprecated; use 'fleetctl get mdm-ab' instead\n") } + return runGetMDMAB(c) + }, + } +} - bm, err := client.GetAppleBM() - if err != nil { - var nfe service.NotFoundErr - if errors.As(err, &nfe) { - log(c, "Error: No Apple Business server token found. Use `fleetctl generate mdm-apple-bm` and then `fleet serve` with `mdm` configuration to automatically enroll macOS hosts to Fleet.\n") - return nil - } - return fmt.Errorf("could not get Apple BM information: %w", err) - } +func getMDMABFlags() []cli.Flag { + return []cli.Flag{ + configFlag(), + contextFlag(), + debugFlag(), + } +} - defaultTeam := bm.DefaultTeam - if defaultTeam == "" { - defaultTeam = "No team" - } - printKeyValueTable(c, [][]string{ - {"Apple ID:", bm.AppleID}, - {"Organization name:", bm.OrgName}, - {"MDM server URL:", bm.MDMServerURL}, - {"Renew date:", bm.RenewDate.Format("January 2, 2006")}, - {"Default team:", defaultTeam}, - }) +func runGetMDMAB(c *cli.Context) error { + const expirationWarning = 30 * 24 * time.Hour // 30 days - warnDate := time.Now().Add(expirationWarning) - if bm.RenewDate.Before(time.Now()) { - // certificate is expired, print an error - color.New(color.FgRed).Fprintln(c.App.Writer, "\nERROR: Your Apple Business (AB) server token is expired. Laptops newly purchased via ABM will not automatically enroll in Fleet. To renew your ABM server token, follow these instructions: https://fleetdm.com/docs/using-fleet/faq#how-can-i-renew-my-apple-business-manager-server-token") - } else if bm.RenewDate.Before(warnDate) { - // certificate will soon expire, print a warning - color.New(color.FgYellow).Fprintln(c.App.Writer, "\nWARNING: Your Apple Business (AB) server token is less than 30 days from expiration. If it expires, laptops newly purchased via ABM will not automatically enroll in Fleet. To renew your ABM server token, follow these instructions: https://fleetdm.com/docs/using-fleet/faq#how-can-i-renew-my-apple-business-manager-server-token") - } + client, err := clientFromCLI(c) + if err != nil { + return err + } + bm, err := client.GetAppleBM() + if err != nil { + var nfe service.NotFoundErr + if errors.As(err, &nfe) { + log(c, "Error: No Apple Business (AB) server token found. Use `fleetctl generate mdm-ab` and then `fleet serve` with `mdm` configuration to automatically enroll macOS hosts to Fleet.\n") return nil - }, + } + return fmt.Errorf("could not get Apple Business information: %w", err) + } + + defaultTeam := bm.DefaultTeam + if defaultTeam == "" { + defaultTeam = "No team" } + printKeyValueTable(c, [][]string{ + {"Apple ID:", bm.AppleID}, + {"Organization name:", bm.OrgName}, + {"MDM server URL:", bm.MDMServerURL}, + {"Renew date:", bm.RenewDate.Format("January 2, 2006")}, + {"Default team:", defaultTeam}, + }) + + warnDate := time.Now().Add(expirationWarning) + if bm.RenewDate.Before(time.Now()) { + // certificate is expired, print an error + color.New(color.FgRed).Fprintln(c.App.Writer, "\nERROR: Your Apple Business (AB) server token is expired. Laptops newly purchased via Apple Business will not automatically enroll in Fleet. To renew your AB server token, follow these instructions: https://fleetdm.com/docs/using-fleet/faq#how-can-i-renew-my-apple-business-manager-server-token") + } else if bm.RenewDate.Before(warnDate) { + // certificate will soon expire, print a warning + color.New(color.FgYellow).Fprintln(c.App.Writer, "\nWARNING: Your Apple Business (AB) server token is less than 30 days from expiration. If it expires, laptops newly purchased via Apple Business will not automatically enroll in Fleet. To renew your AB server token, follow these instructions: https://fleetdm.com/docs/using-fleet/faq#how-can-i-renew-my-apple-business-manager-server-token") + } + + return nil } func getMDMCommandResultsCommand() *cli.Command { diff --git a/cmd/fleetctl/fleetctl/get_test.go b/cmd/fleetctl/fleetctl/get_test.go index 012f43d0cfc..20901db5a7b 100644 --- a/cmd/fleetctl/fleetctl/get_test.go +++ b/cmd/fleetctl/fleetctl/get_test.go @@ -2470,7 +2470,7 @@ func TestGetAppleBM(t *testing.T) { t.Run("free license", func(t *testing.T) { testing_utils.RunServerWithMockedDS(t) - expected := `could not get Apple BM information: missing or invalid license` + expected := `could not get Apple Business information: missing or invalid license` _, err := runAppNoChecks([]string{"get", "mdm_apple_bm"}) require.Error(t, err) assert.Contains(t, err.Error(), expected) @@ -2501,7 +2501,7 @@ func TestGetAppleBM(t *testing.T) { } out := runAppForTest(t, []string{"get", "mdm_apple_bm"}) - assert.Contains(t, out, "No Apple Business server token found.") + assert.Contains(t, out, "No Apple Business (AB) server token found.") }) t.Run("premium license, multiple tokens", func(t *testing.T) { @@ -2515,7 +2515,7 @@ func TestGetAppleBM(t *testing.T) { } _, err := runAppNoChecks([]string{"get", "mdm_apple_bm"}) - assert.ErrorContains(t, err, "This API endpoint has been deprecated. Please use the new GET /abm_tokens API endpoint") + assert.ErrorContains(t, err, "This API endpoint has been deprecated. Please use the new GET /ab_tokens API endpoint") }) } diff --git a/cmd/fleetctl/fleetctl/gitops.go b/cmd/fleetctl/fleetctl/gitops.go index 9a1e37a08bb..c23149978b5 100644 --- a/cmd/fleetctl/fleetctl/gitops.go +++ b/cmd/fleetctl/fleetctl/gitops.go @@ -507,7 +507,7 @@ func gitopsCommand() *cli.Command { if hasMissingABMTeam { if mdm, ok := config.OrgSettings["mdm"]; ok { if mdmMap, ok := mdm.(map[string]any); ok { - if appleBM, ok := mdmMap["apple_business_manager"]; ok { + if appleBM, ok := mdmMap["apple_business"]; ok { if bmSettings, ok := appleBM.([]any); ok { originalABMConfig = bmSettings } @@ -515,7 +515,7 @@ func gitopsCommand() *cli.Command { // If team is not found, we need to remove the AppleBMDefaultTeam from // the global config, and then apply it after teams are processed - mdmMap["apple_business_manager"] = nil + mdmMap["apple_business"] = nil mdmMap["apple_bm_default_team"] = "" } } @@ -662,7 +662,7 @@ func gitopsCommand() *cli.Command { if usesLegacyABMConfig { return fmt.Errorf("apple_bm_default_team %s cannot be deleted", team.Name) } - return fmt.Errorf("apple_business_manager team %s cannot be deleted", team.Name) + return fmt.Errorf("apple_business team %s cannot be deleted", team.Name) } if slices.Contains(vppTeams, team.Name) { return fmt.Errorf("volume_purchasing_program team %s cannot be deleted", team.Name) @@ -1005,7 +1005,10 @@ func checkABMTeamAssignments(config *spec.GitOps, fleetClient *service.Client) ( if mdm, ok := config.OrgSettings["mdm"]; ok { if mdmMap, ok := mdm.(map[string]any); ok { appleBMDT, hasLegacyConfig := mdmMap["apple_bm_default_team"] - appleBM, hasNewConfig := mdmMap["apple_business_manager"] + // After ApplyDeprecatedKeyMappings runs, any legacy + // "apple_business_manager" key has already been migrated to + // "apple_business", so we only look up the new name here. + appleBM, hasNewConfig := mdmMap["apple_business"] if hasLegacyConfig && hasNewConfig { return nil, false, false, errors.New(fleet.AppleABMDefaultTeamDeprecatedMessage) @@ -1100,13 +1103,13 @@ func applyABMTokenAssignmentIfNeeded( } else { for _, abmTeam := range abmTeamNames { if !slices.Contains(teamNames, abmTeam) { - return fmt.Errorf("apple_business_manager team %q not found in team configs", abmTeam) + return fmt.Errorf("apple_business team %q not found in team configs", abmTeam) } } appConfigUpdate = map[string]map[string]any{ "mdm": { - "apple_business_manager": originalMDMConfig, + "apple_business": originalMDMConfig, }, } } diff --git a/cmd/fleetctl/fleetctl/gitops_test.go b/cmd/fleetctl/fleetctl/gitops_test.go index 73ea2399736..03140c9eb7d 100644 --- a/cmd/fleetctl/fleetctl/gitops_test.go +++ b/cmd/fleetctl/fleetctl/gitops_test.go @@ -3906,10 +3906,10 @@ software: workstations, }, dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) { - assert.ErrorContains(t, err, "apple_business_manager team \"📱🏢 Company-owned iPhones\" not found in team configs") + assert.ErrorContains(t, err, "apple_business team \"📱🏢 Company-owned iPhones\" not found in team configs") }, realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) { - assert.ErrorContains(t, err, "apple_business_manager team \"📱🏢 Company-owned iPhones\" not found in team configs") + assert.ErrorContains(t, err, "apple_business team \"📱🏢 Company-owned iPhones\" not found in team configs") }, }, { diff --git a/cmd/fleetctl/fleetctl/templates/new/default.template.yml b/cmd/fleetctl/fleetctl/templates/new/default.template.yml index a16b8fc2b47..82413c90f68 100644 --- a/cmd/fleetctl/fleetctl/templates/new/default.template.yml +++ b/cmd/fleetctl/fleetctl/templates/new/default.template.yml @@ -67,7 +67,7 @@ org_settings: # • https://fleetdm.com/docs/configuration/yaml-files#apple-business-manager # • https://fleetdm.com/guides/apple-mdm-setup#apple-business-manager-abm ########################################################### - # apple_business_manager: + # apple_business: # - organization_name: "My Company, Inc." # This must exactly match the organization name in Apple Business (AB). # macos_fleet: "💻 Workstations" # Where new macOS devices from AB will appear diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigJson.json index 3f5357794fb..0c51587c048 100644 --- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -120,6 +120,7 @@ "apple_server_url": "", "apple_bm_enabled_and_configured": false, "enabled_and_configured": false, + "apple_business": null, "apple_business_manager": null, "volume_purchasing_program": null, "windows_enabled_and_configured": false, diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json index f37566a835d..88fdfeb43a2 100644 --- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json +++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json @@ -92,6 +92,7 @@ "apple_server_url": "", "apple_bm_enabled_and_configured": false, "enabled_and_configured": false, + "apple_business": null, "apple_business_manager": null, "volume_purchasing_program": null, "windows_enabled_and_configured": false, diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml index ecf44e85ece..f15d075fb6d 100644 --- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml +++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml @@ -37,6 +37,7 @@ spec: apple_server_url: "" apple_bm_enabled_and_configured: false enabled_and_configured: false + apple_business: null apple_business_manager: null volume_purchasing_program: null windows_enabled_and_configured: false diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index e706c033912..7a39b06a748 100644 --- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -37,6 +37,7 @@ spec: apple_server_url: "" apple_bm_enabled_and_configured: false enabled_and_configured: false + apple_business: null apple_business_manager: null volume_purchasing_program: null windows_enabled_and_configured: false diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index 27baa583ddb..ba8f385a78e 100644 --- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -66,6 +66,7 @@ }, "mdm": { "android_enabled_and_configured": false, + "apple_business": null, "apple_business_manager": null, "apple_server_url": "", "volume_purchasing_program": null, diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index 3403cfbdfb6..31b0c77bea9 100644 --- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -33,6 +33,7 @@ spec: zendesk: null mdm: android_enabled_and_configured: false + apple_business: null apple_business_manager: null apple_server_url: "" volume_purchasing_program: null diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml index e8f94636ffa..87ca7a2c744 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml @@ -77,7 +77,7 @@ integrations: group_id: 123456789 url: https://some-zendesk-url.com mdm: - apple_business_manager: + apple_business: - ios_fleet: "\U0001F4F1\U0001F3E2 Company-owned mobile devices" ipados_fleet: "\U0001F4F1\U0001F3E2 Company-owned mobile devices" macos_fleet: "\U0001F4BB Workstations" diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml index 23d10d299c7..26e986f35dd 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml @@ -76,7 +76,7 @@ integrations: group_id: 123456789 url: https://some-zendesk-url.com mdm: - apple_business_manager: + apple_business: - ios_fleet: "\U0001F4F1\U0001F3E2 Company-owned mobile devices" ipados_fleet: "\U0001F4F1\U0001F3E2 Company-owned mobile devices" macos_fleet: "\U0001F4BB Workstations" diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml index 50874004885..4e94da717b4 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml @@ -110,7 +110,7 @@ org_settings: group_id: 123456789 url: https://some-zendesk-url.com mdm: - apple_business_manager: + apple_business: - ios_fleet: "📱🏢 Company-owned mobile devices" ipados_fleet: "📱🏢 Company-owned mobile devices" macos_fleet: "💻 Workstations" diff --git a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 2f6c31c30c8..993a865fd20 100644 --- a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -33,6 +33,7 @@ spec: zendesk: null mdm: android_enabled_and_configured: false + apple_business: apple_business_manager: apple_server_url: "" volume_purchasing_program: diff --git a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index 0193ee9b555..9e463404154 100644 --- a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -33,6 +33,7 @@ spec: zendesk: null mdm: android_enabled_and_configured: false + apple_business: apple_business_manager: apple_server_url: "" volume_purchasing_program: diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 331bbba5e98..b899f20155b 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -63,7 +63,7 @@ func (svc *Service) GetAppleBM(ctx context.Context) (*fleet.AppleBM, error) { } if len(tokens) > 1 { - return nil, errors.New("This API endpoint has been deprecated. Please use the new GET /abm_tokens API endpoint documented here: https://fleetdm.com/learn-more-about/apple-business-manager-tokens-api") + return nil, errors.New("This API endpoint has been deprecated. Please use the new GET /ab_tokens API endpoint documented here: https://fleetdm.com/learn-more-about/apple-business-manager-tokens-api") } abmToken := tokens[0] diff --git a/frontend/components/App/App.tsx b/frontend/components/App/App.tsx index 304dd8bfd0f..38d3ddfc64e 100644 --- a/frontend/components/App/App.tsx +++ b/frontend/components/App/App.tsx @@ -18,7 +18,7 @@ import usersAPI from "services/entities/users"; import configAPI from "services/entities/config"; import hostCountAPI from "services/entities/host_count"; import mdmAppleBMAPI, { - IGetAbmTokensResponse, + IGetAbTokensResponse, } from "services/entities/mdm_apple_bm"; import mdmAppleAPI, { IGetVppTokensResponse, @@ -115,18 +115,18 @@ const App = ({ children, location }: IAppProps): JSX.Element => { }, }); - // Get the ABM tokens - useQuery( - ["abm_tokens"], + // Get the Apple Business (AB) tokens + useQuery( + ["ab_tokens"], () => mdmAppleBMAPI.getTokens(), { ...DEFAULT_USE_QUERY_OPTIONS, enabled: !!isGlobalAdmin && !!config?.mdm.enabled_and_configured, - onSuccess: ({ abm_tokens }) => { - abm_tokens.length && + onSuccess: ({ ab_tokens }) => { + ab_tokens.length && setABMExpiry({ - earliestExpiry: getEarliestExpiry(abm_tokens), - needsAbmTermsRenewal: abm_tokens.some( + earliestExpiry: getEarliestExpiry(ab_tokens), + needsAbmTermsRenewal: ab_tokens.some( (token) => token.terms_expired ), }); diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index 6d86f28f57a..9a4814f0033 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -21,7 +21,7 @@ export type ITokenTeam = { name: string; }; -export interface IMdmAbmToken { +export interface IMdmAbToken { id: number; apple_id: string; org_name: string; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/AppleBusinessManagerPage.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/AppleBusinessManagerPage.tsx index 2c973c6a40b..ebab83a6be3 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/AppleBusinessManagerPage.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/AppleBusinessManagerPage.tsx @@ -8,9 +8,9 @@ import { AxiosError } from "axios"; import PATHS from "router/paths"; import { AppContext } from "context/app"; -import { IMdmAbmToken } from "interfaces/mdm"; +import { IMdmAbToken } from "interfaces/mdm"; import mdmAbmAPI, { - IGetAbmTokensResponse, + IGetAbTokensResponse, } from "services/entities/mdm_apple_bm"; import BackButton from "components/BackButton"; @@ -52,22 +52,22 @@ const AppleBusinessManagerPage = ({ router }: { router: InjectedRouter }) => { const [showAddAbmModal, setShowAddAbmModal] = useState(false); const [showEditTeamsModal, setShowEditTeamsModal] = useState(false); - const selectedToken = useRef(null); + const selectedToken = useRef(null); const { - data: abmTokens, + data: abTokens, error: errorAbmTokens, isLoading, isRefetching, refetch, - } = useQuery( - ["abmTokens"], + } = useQuery( + ["abTokens"], () => mdmAbmAPI.getTokens(), { refetchOnWindowFocus: false, retry: (tries, error) => error.status !== 404 && error.status !== 400 && tries <= 3, - select: (data) => data?.abm_tokens, + select: (data) => data?.ab_tokens, onSuccess: (data) => { // we need to call setABMExpiry here to update the expiry info so the terms banner // displays correctly @@ -84,7 +84,7 @@ const AppleBusinessManagerPage = ({ router }: { router: InjectedRouter }) => { } ); - const onEditTokenTeam = (abmToken: IMdmAbmToken) => { + const onEditTokenTeam = (abmToken: IMdmAbToken) => { selectedToken.current = abmToken; setShowEditTeamsModal(true); }; @@ -109,7 +109,7 @@ const AppleBusinessManagerPage = ({ router }: { router: InjectedRouter }) => { setShowAddAbmModal(false); }; - const onRenewToken = (abmToken: IMdmAbmToken) => { + const onRenewToken = (abmToken: IMdmAbToken) => { selectedToken.current = abmToken; setShowRenewModal(true); }; @@ -125,7 +125,7 @@ const AppleBusinessManagerPage = ({ router }: { router: InjectedRouter }) => { setShowRenewModal(false); }, [refetch]); - const onDeleteToken = (abmToken: IMdmAbmToken) => { + const onDeleteToken = (abmToken: IMdmAbToken) => { selectedToken.current = abmToken; setShowDeleteModal(true); }; @@ -175,11 +175,11 @@ const AppleBusinessManagerPage = ({ router }: { router: InjectedRouter }) => { return ; } - if (abmTokens?.length === 0) { + if (abTokens?.length === 0) { return ; } - if (abmTokens) { + if (abTokens) { return ( <>

@@ -188,7 +188,7 @@ const AppleBusinessManagerPage = ({ router }: { router: InjectedRouter }) => { hosts.

{

Apple Business (AB)

{isPremiumTier && - abmTokens?.length !== 0 && + abTokens?.length !== 0 && !!config?.mdm.enabled_and_configured && ( )} diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTable.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTable.tsx index 430a03d5b0e..62badd3eeac 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTable.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTable.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { IMdmAbmToken } from "interfaces/mdm"; +import { IMdmAbToken } from "interfaces/mdm"; import useGitOpsMode from "hooks/useGitOpsMode"; import TableContainer from "components/TableContainer"; @@ -10,21 +10,21 @@ import { generateTableConfig } from "./AppleBusinessManagerTableConfig"; const baseClass = "apple-business-manager-table"; interface IAppleBusinessManagerTableProps { - abmTokens: IMdmAbmToken[]; - onEditTokenTeam: (token: IMdmAbmToken) => void; - onRenewToken: (token: IMdmAbmToken) => void; - onDeleteToken: (token: IMdmAbmToken) => void; + abTokens: IMdmAbToken[]; + onEditTokenTeam: (token: IMdmAbToken) => void; + onRenewToken: (token: IMdmAbToken) => void; + onDeleteToken: (token: IMdmAbToken) => void; } const AppleBusinessManagerTable = ({ - abmTokens, + abTokens, onEditTokenTeam, onRenewToken, onDeleteToken, }: IAppleBusinessManagerTableProps) => { const { gitOpsModeEnabled, repoURL } = useGitOpsMode(); - const onSelectAction = (action: string, abmToken: IMdmAbmToken) => { + const onSelectAction = (action: string, abmToken: IMdmAbToken) => { switch (action) { case "editTeams": onEditTokenTeam(abmToken); @@ -47,7 +47,7 @@ const AppleBusinessManagerTable = ({ ); return ( - + columnConfigs={tableConfig} defaultSortHeader="org_name" disableTableHeader @@ -56,7 +56,7 @@ const AppleBusinessManagerTable = ({ isAllPagesSelected={false} emptyComponent={() => <>} isLoading={false} - data={abmTokens} + data={abTokens} className={baseClass} /> ); diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx index f05c943091d..485706ba7fe 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx @@ -1,7 +1,7 @@ import React from "react"; import { CellProps, Column } from "react-table"; -import { IMdmAbmToken } from "interfaces/mdm"; +import { IMdmAbToken } from "interfaces/mdm"; import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config"; import { getTeamDisplayName } from "interfaces/team"; import { IDropdownOption } from "interfaces/dropdownOption"; @@ -16,11 +16,11 @@ import RenewDateCell from "../../../components/RenewDateCell"; import OrgNameCell from "./OrgNameCell"; import { IRenewDateCellStatusConfig } from "../../../components/RenewDateCell/RenewDateCell"; -type IAbmTableConfig = Column; -type ITableStringCellProps = IStringCellProps; -type IRenewDateCellProps = CellProps; +type IAbmTableConfig = Column; +type ITableStringCellProps = IStringCellProps; +type IRenewDateCellProps = CellProps; -type ITableHeaderProps = IHeaderProps; +type ITableHeaderProps = IHeaderProps; const DEFAULT_ACTION_OPTIONS: IDropdownOption[] = [ { value: "editTeams", label: "Edit fleets", disabled: false }, @@ -71,7 +71,7 @@ const RENEW_DATE_CELL_STATUS_CONFIG: IRenewDateCellStatusConfig = { }; export const generateTableConfig = ( - actionSelectHandler: (value: string, team: IMdmAbmToken) => void, + actionSelectHandler: (value: string, team: IMdmAbToken) => void, gitopsModeEnabled: boolean, repoURL?: string ): IAbmTableConfig[] => { @@ -208,6 +208,6 @@ export const generateTableConfig = ( ]; }; -export const generateTableData = (data: IMdmAbmToken[]) => { +export const generateTableData = (data: IMdmAbToken[]) => { return data; }; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/EditTeamsAbmModal/EditTeamsAbmModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/EditTeamsAbmModal/EditTeamsAbmModal.tsx index 5168ae19825..53bce1c2cc1 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/EditTeamsAbmModal/EditTeamsAbmModal.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/EditTeamsAbmModal/EditTeamsAbmModal.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useContext, useMemo, useState } from "react"; import { AppContext } from "context/app"; import { NotificationContext } from "context/notification"; -import { IMdmAbmToken } from "interfaces/mdm"; +import { IMdmAbToken } from "interfaces/mdm"; import { ITeamSummary } from "interfaces/team"; import mdmAbmAPI from "services/entities/mdm_apple_bm"; @@ -16,7 +16,7 @@ import Button from "components/buttons/Button"; const baseClass = "edit-teams-abm-modal"; interface IEditTeamsAbmModalProps { - token: IMdmAbmToken; + token: IMdmAbToken; onCancel: () => void; onSuccess: () => void; } @@ -38,9 +38,9 @@ export const getOptions = (availableTeams: ITeamSummary[] = []) => { * returned by the get token API. */ interface SelectedTeamNames { - ios_team: IMdmAbmToken["ios_team"]["name"]; - ipados_team: IMdmAbmToken["ipados_team"]["name"]; - macos_team: IMdmAbmToken["macos_team"]["name"]; + ios_team: IMdmAbToken["ios_team"]["name"]; + ipados_team: IMdmAbToken["ipados_team"]["name"]; + macos_team: IMdmAbToken["macos_team"]["name"]; } /** diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx index eaf2752af7b..7304097fc6d 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx @@ -1,7 +1,7 @@ import React from "react"; import { CellProps, Column } from "react-table"; -import { IMdmAbmToken, IMdmVppToken } from "interfaces/mdm"; +import { IMdmAbToken, IMdmVppToken } from "interfaces/mdm"; import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config"; import { IDropdownOption } from "interfaces/dropdownOption"; @@ -148,6 +148,6 @@ export const generateTableConfig = ( ]; }; -export const generateTableData = (data: IMdmAbmToken[]) => { +export const generateTableData = (data: IMdmAbToken[]) => { return data; }; diff --git a/frontend/services/entities/mdm_apple_bm.ts b/frontend/services/entities/mdm_apple_bm.ts index 604eba465b7..5716d86071c 100644 --- a/frontend/services/entities/mdm_apple_bm.ts +++ b/frontend/services/entities/mdm_apple_bm.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { IMdmAbmToken } from "interfaces/mdm"; +import { IMdmAbToken } from "interfaces/mdm"; import sendRequest from "services"; import endpoints from "utilities/endpoints"; -export interface IAppleBusinessManagerTokenFormData { +export interface IAppleBusinessTokenFormData { token: File | null; } @@ -15,12 +15,12 @@ export interface IGetAppleBMInfoResponse { renew_date: string; } -export interface IGetAbmTokensResponse { - abm_tokens: IMdmAbmToken[]; +export interface IGetAbTokensResponse { + ab_tokens: IMdmAbToken[]; } -export interface IAbmTokenResponse { - abm_token: IMdmAbmToken; +export interface IAbTokenResponse { + ab_token: IMdmAbToken; } export default { @@ -55,17 +55,17 @@ export default { return sendRequest("GET", MDM_APPLE_ABM_PUBLIC_KEY); }, - uploadToken: (token: File): Promise => { - const { MDM_ABM_TOKENS } = endpoints; + uploadToken: (token: File): Promise => { + const { MDM_AB_TOKENS } = endpoints; const formData = new FormData(); formData.append("token", token); - return sendRequest("POST", MDM_ABM_TOKENS, formData); + return sendRequest("POST", MDM_AB_TOKENS, formData); }, - renewToken: (id: number, token: File): Promise => { - const { MDM_ABM_TOKEN_RENEW } = endpoints; - const path = MDM_ABM_TOKEN_RENEW(id); + renewToken: (id: number, token: File): Promise => { + const { MDM_AB_TOKEN_RENEW } = endpoints; + const path = MDM_AB_TOKEN_RENEW(id); const formData = new FormData(); formData.append("token", token); @@ -74,14 +74,14 @@ export default { }, deleteToken: (id: number): Promise => { - const { MDM_ABM_TOKEN } = endpoints; - const path = MDM_ABM_TOKEN(id); + const { MDM_AB_TOKEN } = endpoints; + const path = MDM_AB_TOKEN(id); return sendRequest("DELETE", path); }, - getTokens: (): Promise => { - const { MDM_ABM_TOKENS } = endpoints; - return sendRequest("GET", MDM_ABM_TOKENS); + getTokens: (): Promise => { + const { MDM_AB_TOKENS } = endpoints; + return sendRequest("GET", MDM_AB_TOKENS); }, editTeams: async (params: { @@ -92,8 +92,8 @@ export default { macos_fleet_id: number; }; }) => { - const { MDM_ABM_TOKEN_TEAMS } = endpoints; - const path = MDM_ABM_TOKEN_TEAMS(params.tokenId); + const { MDM_AB_TOKEN_TEAMS } = endpoints; + const path = MDM_AB_TOKEN_TEAMS(params.tokenId); return sendRequest("PATCH", path, params.teams); }, }; diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index 6cf2abd3e5c..237277dbf80 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -144,12 +144,12 @@ export default { MDM_APPLE: `/${API_VERSION}/fleet/mdm/apple`, // Apple Business (AB) endpoints - MDM_ABM_TOKENS: `/${API_VERSION}/fleet/abm_tokens`, - MDM_ABM_TOKEN: (id: number) => `/${API_VERSION}/fleet/abm_tokens/${id}`, - MDM_ABM_TOKEN_RENEW: (id: number) => - `/${API_VERSION}/fleet/abm_tokens/${id}/renew`, - MDM_ABM_TOKEN_TEAMS: (id: number) => - `/${API_VERSION}/fleet/abm_tokens/${id}/fleets`, + MDM_AB_TOKENS: `/${API_VERSION}/fleet/ab_tokens`, + MDM_AB_TOKEN: (id: number) => `/${API_VERSION}/fleet/ab_tokens/${id}`, + MDM_AB_TOKEN_RENEW: (id: number) => + `/${API_VERSION}/fleet/ab_tokens/${id}/renew`, + MDM_AB_TOKEN_TEAMS: (id: number) => + `/${API_VERSION}/fleet/ab_tokens/${id}/fleets`, MDM_APPLE_ABM_PUBLIC_KEY: `/${API_VERSION}/fleet/mdm/apple/abm_public_key`, MDM_APPLE_APNS_CERTIFICATE: `/${API_VERSION}/fleet/mdm/apple/apns_certificate`, MDM_APPLE_PNS: `/${API_VERSION}/fleet/apns`, diff --git a/pkg/spec/gitops_deprecations.go b/pkg/spec/gitops_deprecations.go index 07003c7f972..c4aa91e48a1 100644 --- a/pkg/spec/gitops_deprecations.go +++ b/pkg/spec/gitops_deprecations.go @@ -12,7 +12,7 @@ import ( // Examples: // - "team_settings" -> "settings" // - "queries" -> "reports" -// - "org_settings.mdm.apple_business_manager[].macos_team" -> "org_settings.mdm.apple_business_manager[].macos_fleet" +// - "org_settings.mdm.apple_business[].macos_team" -> "org_settings.mdm.apple_business[].macos_fleet" type DeprecatedKeyMapping struct { OldPath string NewPath string @@ -58,10 +58,15 @@ var DeprecatedGitOpsKeyMappings = []DeprecatedKeyMapping{ {"org_settings.org_info.org_logo_url", "org_settings.org_info.org_logo_url_dark_mode"}, {"org_settings.org_info.org_logo_url_light_background", "org_settings.org_info.org_logo_url_light_mode"}, - // Nested keys in org_settings.mdm.apple_business_manager[] - {"org_settings.mdm.apple_business_manager[].macos_team", "org_settings.mdm.apple_business_manager[].macos_fleet"}, - {"org_settings.mdm.apple_business_manager[].ios_team", "org_settings.mdm.apple_business_manager[].ios_fleet"}, - {"org_settings.mdm.apple_business_manager[].ipados_team", "org_settings.mdm.apple_business_manager[].ipados_fleet"}, + // org_settings.mdm.apple_business_manager -> apple_business (parent rename + // runs before the nested children below so they resolve against the new + // parent name). + {"org_settings.mdm.apple_business_manager", "org_settings.mdm.apple_business"}, + + // Nested keys in org_settings.mdm.apple_business[] + {"org_settings.mdm.apple_business[].macos_team", "org_settings.mdm.apple_business[].macos_fleet"}, + {"org_settings.mdm.apple_business[].ios_team", "org_settings.mdm.apple_business[].ios_fleet"}, + {"org_settings.mdm.apple_business[].ipados_team", "org_settings.mdm.apple_business[].ipados_fleet"}, // Nested keys in org_settings.mdm.volume_purchasing_program[] {"org_settings.mdm.volume_purchasing_program[].teams", "org_settings.mdm.volume_purchasing_program[].fleets"}, diff --git a/pkg/spec/gitops_test.go b/pkg/spec/gitops_test.go index 158fd4d30f4..8c8e3f19ff5 100644 --- a/pkg/spec/gitops_test.go +++ b/pkg/spec/gitops_test.go @@ -3748,6 +3748,79 @@ org_settings: }) } +// TestAppleBusinessKeyRename verifies that the new mdm.apple_business key is +// accepted in org_settings, that the deprecated mdm.apple_business_manager key +// still works, and that specifying both raises a conflict error. +func TestAppleBusinessKeyRename(t *testing.T) { + t.Parallel() + + baseConfig := func(mdmSection string) string { + return ` +controls: +reports: +policies: +agent_options: +org_settings: + server_settings: + server_url: https://fleet.example.com + org_info: + contact_url: https://example.com/contact + org_name: Test Org + secrets: + mdm:` + mdmSection + ` +` + } + + t.Run("new_key_accepted", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + yaml := baseConfig(` + apple_business: + - organization_name: Test Org + macos_fleet: "Workstations" + ios_fleet: "Phones" + ipados_fleet: "Tablets"`) + yamlPath := filepath.Join(dir, "gitops.yml") + require.NoError(t, os.WriteFile(yamlPath, []byte(yaml), 0o644)) + + _, err := GitOpsFromFile(yamlPath, dir, nil, nopLogf) + require.NoError(t, err) + }) + + t.Run("old_key_still_accepted", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + yaml := baseConfig(` + apple_business_manager: + - organization_name: Test Org + macos_fleet: "Workstations" + ios_fleet: "Phones" + ipados_fleet: "Tablets"`) + yamlPath := filepath.Join(dir, "gitops.yml") + require.NoError(t, os.WriteFile(yamlPath, []byte(yaml), 0o644)) + + _, err := GitOpsFromFile(yamlPath, dir, nil, nopLogf) + require.NoError(t, err) + }) + + t.Run("both_keys_conflict", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + yaml := baseConfig(` + apple_business: + - organization_name: A + apple_business_manager: + - organization_name: B`) + yamlPath := filepath.Join(dir, "gitops.yml") + require.NoError(t, os.WriteFile(yamlPath, []byte(yaml), 0o644)) + + _, err := GitOpsFromFile(yamlPath, dir, nil, nopLogf) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot specify both") + require.Contains(t, err.Error(), "org_settings.mdm.apple_business") + }) +} + // TestSetupExperienceSoftwareDeprecation verifies that supplying a list of // software under `controls.setup_experience.software` emits a deprecation // warning steering users toward the per-item `setup_experience: true` form. diff --git a/server/fleet/app.go b/server/fleet/app.go index 87afba343a9..39287aed5bf 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -191,9 +191,10 @@ type MDM struct { // Deprecated: use AppleBussinessManager instead DeprecatedAppleBMDefaultTeam string `json:"apple_bm_default_team,omitempty"` //nolint:apiparamcheck // not renaming already-deprecated field - // AppleBusinessManager defines the associations between ABM tokens - // and the teams used to assign hosts when they're ingested from ABM. - AppleBusinessManager optjson.Slice[MDMAppleABMAssignmentInfo] `json:"apple_business_manager"` + // AppleBusinessManager defines the associations between AB tokens + // and the fleets used to assign hosts when they're ingested from Apple + // Business. + AppleBusinessManager optjson.Slice[MDMAppleABMAssignmentInfo] `json:"apple_business_manager" renameto:"apple_business"` // AppleBMEnabledAndConfigured is set to true if Fleet has been // configured with the required Apple BM key pair or token. It can't be set diff --git a/server/fleet/errors.go b/server/fleet/errors.go index 8b53da057f3..d7993f2d9c3 100644 --- a/server/fleet/errors.go +++ b/server/fleet/errors.go @@ -24,7 +24,7 @@ var ( WindowsMDMNotConfiguredMessage = "Windows MDM isn't turned on. For more information about setting up MDM, please visit https://fleetdm.com/learn-more-about/windows-mdm" AndroidMDMNotConfiguredMessage = "Android MDM isn't turned on. For more information about setting up MDM, please visit https://fleetdm.com/learn-more-about/how-to-connect-android-enterprise" AppleMDMNotConfiguredMessage = "macOS MDM isn't turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM." - AppleABMDefaultTeamDeprecatedMessage = "mdm.apple_bm_default_team has been deprecated. Please use the new mdm.apple_business_manager key documented here: https://fleetdm.com/learn-more-about/apple-business-manager-gitops" + AppleABMDefaultTeamDeprecatedMessage = "mdm.apple_bm_default_team has been deprecated. Please use the new mdm.apple_business key documented here: https://fleetdm.com/learn-more-about/apple-business-manager-gitops" AppleOSVersionUnsupportedMessage = "The minimum version isn't supported by Apple." AppleOSVersionDeadlineInvalidMessage = "The deadline isn't a valid date." CantTurnOffMDMForWindowsHostsMessage = "Can't turn off MDM for Windows hosts." diff --git a/server/service/appconfig.go b/server/service/appconfig.go index dc5a6534ce8..7b14ffbb099 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -1876,7 +1876,7 @@ func (svc *Service) validateABMAssignments( if mdm.AppleBusinessManager.Set && len(mdm.AppleBusinessManager.Value) > 0 { if !lic.IsPremium() { - invalid.Append("mdm.apple_business_manager", ErrMissingLicense.Error()) + invalid.Append("mdm.apple_business", ErrMissingLicense.Error()) return nil, nil } @@ -1909,13 +1909,13 @@ func (svc *Service) validateABMAssignments( for _, bm := range mdm.AppleBusinessManager.Value { for _, tmName := range []string{bm.MacOSTeam, bm.IOSTeam, bm.IpadOSTeam} { if _, ok := teamsByName[norm.NFC.String(tmName)]; !ok { - invalid.Appendf("mdm.apple_business_manager", "team %s doesn't exist", tmName) + invalid.Appendf("mdm.apple_business", "team %s doesn't exist", tmName) return nil, nil } } if _, ok := tokensByName[norm.NFC.String(bm.OrganizationName)]; !ok { - invalid.Appendf("mdm.apple_business_manager", "token with organization name %s doesn't exist", bm.OrganizationName) + invalid.Appendf("mdm.apple_business", "token with organization name %s doesn't exist", bm.OrganizationName) return nil, nil } diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 20e8d45a4cd..e7c00f00e9c 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -6884,7 +6884,7 @@ func (uploadABMTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) } type uploadABMTokenResponse struct { - Token *fleet.ABMToken `json:"abm_token,omitempty"` + Token *fleet.ABMToken `json:"abm_token,omitempty" renameto:"ab_token"` Err error `json:"error,omitempty"` } @@ -6954,7 +6954,7 @@ func (svc *Service) DeleteABMToken(ctx context.Context, tokenID uint) error { type listABMTokensResponse struct { Err error `json:"error,omitempty"` - Tokens []*fleet.ABMToken `json:"abm_tokens"` + Tokens []*fleet.ABMToken `json:"abm_tokens" renameto:"ab_tokens"` } func (r listABMTokensResponse) Error() error { return r.Err } @@ -7021,7 +7021,7 @@ type updateABMTokenTeamsRequest struct { } type updateABMTokenTeamsResponse struct { - ABMToken *fleet.ABMToken `json:"abm_token,omitempty"` + ABMToken *fleet.ABMToken `json:"abm_token,omitempty" renameto:"ab_token"` Err error `json:"error,omitempty"` } @@ -7084,7 +7084,7 @@ func (renewABMTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) } type renewABMTokenResponse struct { - ABMToken *fleet.ABMToken `json:"abm_token,omitempty"` + ABMToken *fleet.ABMToken `json:"abm_token,omitempty" renameto:"ab_token"` Err error `json:"error,omitempty"` } diff --git a/server/service/client.go b/server/service/client.go index ca218f4c9eb..a71934e4d15 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -2139,8 +2139,8 @@ func (c *Client) DoGitOps( } if _, ok := mdmAppConfig["apple_bm_default_team"]; !ok && appConfig.License.IsPremium() { - if _, ok := mdmAppConfig["apple_business_manager"]; !ok { - mdmAppConfig["apple_business_manager"] = []interface{}{} + if _, ok := mdmAppConfig["apple_business"]; !ok { + mdmAppConfig["apple_business"] = []any{} } } diff --git a/server/service/client_mdm.go b/server/service/client_mdm.go index 12f3ad52ac2..4155062be01 100644 --- a/server/service/client_mdm.go +++ b/server/service/client_mdm.go @@ -49,7 +49,7 @@ func (c *Client) GetVPPTokens() ([]*fleet.VPPTokenDB, error) { } func (c *Client) CountABMTokens() (int, error) { - verb, path := "GET", "/api/latest/fleet/abm_tokens/count" + verb, path := "GET", "/api/latest/fleet/ab_tokens/count" var responseBody countABMTokensResponse err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, "") return responseBody.Count, err diff --git a/server/service/handler.go b/server/service/handler.go index 14a20e4791a..42d3a19a873 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -837,12 +837,12 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // new flow described in https://github.com/fleetdm/fleet/issues/10383 ue.POST("/api/_version_/fleet/mdm/apple/dep/key_pair", newMDMAppleDEPKeyPairEndpoint, nil) ue.GET("/api/_version_/fleet/mdm/apple/abm_public_key", generateABMKeyPairEndpoint, nil) - ue.POST("/api/_version_/fleet/abm_tokens", uploadABMTokenEndpoint, uploadABMTokenRequest{}) - ue.DELETE("/api/_version_/fleet/abm_tokens/{id:[0-9]+}", deleteABMTokenEndpoint, deleteABMTokenRequest{}) - ue.GET("/api/_version_/fleet/abm_tokens", listABMTokensEndpoint, nil) - ue.GET("/api/_version_/fleet/abm_tokens/count", countABMTokensEndpoint, nil) - ue.PATCH("/api/_version_/fleet/abm_tokens/{id:[0-9]+}/fleets", updateABMTokenTeamsEndpoint, updateABMTokenTeamsRequest{}) - ue.PATCH("/api/_version_/fleet/abm_tokens/{id:[0-9]+}/renew", renewABMTokenEndpoint, renewABMTokenRequest{}) + ue.POST("/api/_version_/fleet/ab_tokens", uploadABMTokenEndpoint, uploadABMTokenRequest{}) + ue.DELETE("/api/_version_/fleet/ab_tokens/{id:[0-9]+}", deleteABMTokenEndpoint, deleteABMTokenRequest{}) + ue.GET("/api/_version_/fleet/ab_tokens", listABMTokensEndpoint, nil) + ue.GET("/api/_version_/fleet/ab_tokens/count", countABMTokensEndpoint, nil) + ue.PATCH("/api/_version_/fleet/ab_tokens/{id:[0-9]+}/fleets", updateABMTokenTeamsEndpoint, updateABMTokenTeamsRequest{}) + ue.PATCH("/api/_version_/fleet/ab_tokens/{id:[0-9]+}/renew", renewABMTokenEndpoint, renewABMTokenRequest{}) ue.GET("/api/_version_/fleet/mdm/apple/request_csr", getMDMAppleCSREndpoint, getMDMAppleCSRRequest{}) ue.POST("/api/_version_/fleet/mdm/apple/apns_certificate", uploadMDMAppleAPNSCertEndpoint, uploadMDMAppleAPNSCertRequest{}) @@ -861,7 +861,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // Deprecated: GET /mdm/apple_bm is now deprecated, replaced by the // GET /abm endpoint. ue.GET("/api/_version_/fleet/mdm/apple_bm", getAppleBMEndpoint, nil) - // Deprecated: GET /abm is now deprecated, replaced by the GET /abm_tokens endpoint. + // Deprecated: GET /abm is now deprecated, replaced by the GET /ab_tokens endpoint. ue.GET("/api/_version_/fleet/abm", getAppleBMEndpoint, nil) // Deprecated: POST /mdm/apple/profiles/batch is now deprecated, replaced by the diff --git a/server/service/handler_deprecated_paths.go b/server/service/handler_deprecated_paths.go index 677b9a6b260..b287730ec12 100644 --- a/server/service/handler_deprecated_paths.go +++ b/server/service/handler_deprecated_paths.go @@ -213,11 +213,37 @@ var deprecatedPathAliases = []eu.DeprecatedPathAlias{ // ---- ABM/VPP token teams → fleets ---- { - Method: "PATCH", PrimaryPath: "/api/_version_/fleet/abm_tokens/{id:[0-9]+}/fleets", - DeprecatedPaths: []string{"/api/_version_/fleet/abm_tokens/{id:[0-9]+}/teams"}, + Method: "PATCH", PrimaryPath: "/api/_version_/fleet/ab_tokens/{id:[0-9]+}/fleets", + DeprecatedPaths: []string{ + "/api/_version_/fleet/ab_tokens/{id:[0-9]+}/teams", + "/api/_version_/fleet/abm_tokens/{id:[0-9]+}/fleets", + "/api/_version_/fleet/abm_tokens/{id:[0-9]+}/teams", + }, }, { Method: "PATCH", PrimaryPath: "/api/_version_/fleet/vpp_tokens/{id}/fleets", DeprecatedPaths: []string{"/api/_version_/fleet/vpp_tokens/{id}/teams"}, }, + + // ---- abm_tokens → ab_tokens ---- + { + Method: "POST", PrimaryPath: "/api/_version_/fleet/ab_tokens", + DeprecatedPaths: []string{"/api/_version_/fleet/abm_tokens"}, + }, + { + Method: "DELETE", PrimaryPath: "/api/_version_/fleet/ab_tokens/{id:[0-9]+}", + DeprecatedPaths: []string{"/api/_version_/fleet/abm_tokens/{id:[0-9]+}"}, + }, + { + Method: "GET", PrimaryPath: "/api/_version_/fleet/ab_tokens", + DeprecatedPaths: []string{"/api/_version_/fleet/abm_tokens"}, + }, + { + Method: "GET", PrimaryPath: "/api/_version_/fleet/ab_tokens/count", + DeprecatedPaths: []string{"/api/_version_/fleet/abm_tokens/count"}, + }, + { + Method: "PATCH", PrimaryPath: "/api/_version_/fleet/ab_tokens/{id:[0-9]+}/renew", + DeprecatedPaths: []string{"/api/_version_/fleet/abm_tokens/{id:[0-9]+}/renew"}, + }, } From 1298670f1024d5e2b0c7f9c9264e7b2d1cea28d7 Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Tue, 2 Jun 2026 13:29:05 -0400 Subject: [PATCH 2/7] Fix a few missed renames --- changes/42508-rename-abm-to-ab | 2 +- frontend/services/entities/mdm_apple_bm.ts | 4 +- frontend/utilities/endpoints.ts | 2 +- server/fleet/app.go | 2 +- server/fleet/apple_mdm.go | 4 +- server/platform/endpointer/endpoint_utils.go | 9 ++- .../endpointer/extract_alias_rules_test.go | 12 +++ .../endpointer/json_key_duplicator.go | 75 ++++++++++++++--- .../endpointer/json_key_duplicator_test.go | 81 +++++++++++++++++++ .../platform/endpointer/json_key_rewriter.go | 11 +++ server/service/apple_mdm.go | 8 +- server/service/client_mdm.go | 2 +- server/service/handler.go | 2 +- server/service/handler_deprecated_paths.go | 4 + server/service/integration_mdm_test.go | 16 ++-- 15 files changed, 205 insertions(+), 29 deletions(-) diff --git a/changes/42508-rename-abm-to-ab b/changes/42508-rename-abm-to-ab index b89d47524e6..1ad3c0bf443 100644 --- a/changes/42508-rename-abm-to-ab +++ b/changes/42508-rename-abm-to-ab @@ -1 +1 @@ -- Renamed Apple Business Manager (ABM) terminology to Apple Business (AB) in the API, GitOps YAML, and `fleetctl` CLI. The new `/api/v1/fleet/ab_tokens` endpoints, `mdm.apple_business` YAML key, and `fleetctl get mdm-ab`/`fleetctl generate mdm-ab` commands are canonical however the now-deprecated `/abm_tokens`, `apple_business_manager`, `mdm-apple-bm` aliases continue to work for backwards compatibility and log a deprecation warning when used. +- Renamed Apple Business Manager (ABM) terminology to Apple Business (AB) in the API, GitOps YAML, and `fleetctl` CLI. The new `/api/v1/fleet/ab_tokens` and `/api/v1/fleet/mdm/apple/ab_public_key` endpoints, `mdm.apple_business` YAML key, and `fleetctl get mdm-ab`/`fleetctl generate mdm-ab` commands are canonical however the now-deprecated `/abm_tokens`, `/mdm/apple/abm_public_key`, `apple_business_manager`, `mdm-apple-bm` aliases continue to work for backwards compatibility and log a deprecation warning when used. diff --git a/frontend/services/entities/mdm_apple_bm.ts b/frontend/services/entities/mdm_apple_bm.ts index 5716d86071c..13ca2779cf7 100644 --- a/frontend/services/entities/mdm_apple_bm.ts +++ b/frontend/services/entities/mdm_apple_bm.ts @@ -51,8 +51,8 @@ export default { }, downloadPublicKey: () => { - const { MDM_APPLE_ABM_PUBLIC_KEY } = endpoints; - return sendRequest("GET", MDM_APPLE_ABM_PUBLIC_KEY); + const { MDM_APPLE_AB_PUBLIC_KEY } = endpoints; + return sendRequest("GET", MDM_APPLE_AB_PUBLIC_KEY); }, uploadToken: (token: File): Promise => { diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index 237277dbf80..7c4c73fe2fd 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -150,7 +150,7 @@ export default { `/${API_VERSION}/fleet/ab_tokens/${id}/renew`, MDM_AB_TOKEN_TEAMS: (id: number) => `/${API_VERSION}/fleet/ab_tokens/${id}/fleets`, - MDM_APPLE_ABM_PUBLIC_KEY: `/${API_VERSION}/fleet/mdm/apple/abm_public_key`, + MDM_APPLE_AB_PUBLIC_KEY: `/${API_VERSION}/fleet/mdm/apple/ab_public_key`, MDM_APPLE_APNS_CERTIFICATE: `/${API_VERSION}/fleet/mdm/apple/apns_certificate`, MDM_APPLE_PNS: `/${API_VERSION}/fleet/apns`, MDM_APPLE_BM: `/${API_VERSION}/fleet/abm`, // TODO: Deprecated? diff --git a/server/fleet/app.go b/server/fleet/app.go index 39287aed5bf..a2bd3071a93 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -194,7 +194,7 @@ type MDM struct { // AppleBusinessManager defines the associations between AB tokens // and the fleets used to assign hosts when they're ingested from Apple // Business. - AppleBusinessManager optjson.Slice[MDMAppleABMAssignmentInfo] `json:"apple_business_manager" renameto:"apple_business"` + AppleBusinessManager optjson.Slice[MDMAppleABMAssignmentInfo] `json:"apple_business_manager" renameto:"apple_business,inline"` // AppleBMEnabledAndConfigured is set to true if Fleet has been // configured with the required Apple BM key pair or token. It can't be set diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 61ae3b21101..4aa5232f423 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -559,8 +559,8 @@ type HostDEPAssignment struct { // DeletedAt is the timestamp when Fleet was notified that device was deleted from the Fleet // MDM server in Apple Busines Manager (AB). DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"` - // ABMTokenID is the ID of the ABM token that was used to make this DEP assignment. - ABMTokenID *uint `db:"abm_token_id" json:"abm_token_id"` + // ABMTokenID is the ID of the AB token that was used to make this DEP assignment. + ABMTokenID *uint `db:"abm_token_id" json:"abm_token_id" renameto:"ab_token_id"` // MDMMigrationDeadline is the deadline for the MDM migration received from ABM on the host's // most recent sync. MDMMigrationDeadline *time.Time `db:"mdm_migration_deadline" json:"mdm_migration_deadline,omitempty"` diff --git a/server/platform/endpointer/endpoint_utils.go b/server/platform/endpointer/endpoint_utils.go index 55a7c6921be..81dbf8138e6 100644 --- a/server/platform/endpointer/endpoint_utils.go +++ b/server/platform/endpointer/endpoint_utils.go @@ -15,6 +15,7 @@ import ( "net/http" "net/url" "reflect" + "slices" "strconv" "strings" "sync" @@ -155,12 +156,16 @@ func extractAliasRulesRecursive(t reflect.Type, seen map[AliasRule]bool, rules * // Check this field for a renameto tag. renameTo, hasRenameTo := structField.Tag.Lookup("renameto") if hasRenameTo && renameTo != "" { + // Split the new key name from options like ",inline". + newKeyName, renameOpts, _ := strings.Cut(renameTo, ",") + inline := slices.Contains(strings.Split(renameOpts, ","), "inline") + jsonTag, hasJSON := structField.Tag.Lookup("json") - if hasJSON && jsonTag != "" && jsonTag != "-" { + if hasJSON && jsonTag != "" && jsonTag != "-" && newKeyName != "" { // Strip options like ",omitempty" from the json tag. jsonFieldName, _, _ := strings.Cut(jsonTag, ",") if jsonFieldName != "" && jsonFieldName != "-" { - rule := AliasRule{OldKey: jsonFieldName, NewKey: renameTo} + rule := AliasRule{OldKey: jsonFieldName, NewKey: newKeyName, Inline: inline} if !seen[rule] { seen[rule] = true *rules = append(*rules, rule) diff --git a/server/platform/endpointer/extract_alias_rules_test.go b/server/platform/endpointer/extract_alias_rules_test.go index ab2ffe93ffb..c517c0d58b8 100644 --- a/server/platform/endpointer/extract_alias_rules_test.go +++ b/server/platform/endpointer/extract_alias_rules_test.go @@ -53,6 +53,18 @@ func (s *extractAliasRulesSuite) TestSingleRenametoTag() { require.Equal(s.T(), []AliasRule{{OldKey: "team_id", NewKey: "group_id"}}, rules) } +func (s *extractAliasRulesSuite) TestRenametoInlineOption() { + type inlineAlias struct { + Tokens []string `json:"abm_tokens" renameto:"ab_tokens,inline"` + TeamID uint `json:"team_id" renameto:"fleet_id"` + } + rules := ExtractAliasRules(inlineAlias{}) + require.Equal(s.T(), []AliasRule{ + {OldKey: "abm_tokens", NewKey: "ab_tokens", Inline: true}, + {OldKey: "team_id", NewKey: "fleet_id"}, + }, rules) +} + func (s *extractAliasRulesSuite) TestMultipleRenametoTags() { type multiAlias struct { TeamID uint `json:"team_id" renameto:"group_id"` diff --git a/server/platform/endpointer/json_key_duplicator.go b/server/platform/endpointer/json_key_duplicator.go index 6374cd6464c..ec09c1fbd8f 100644 --- a/server/platform/endpointer/json_key_duplicator.go +++ b/server/platform/endpointer/json_key_duplicator.go @@ -20,6 +20,20 @@ type DuplicateJSONKeysOpts struct { // JSON contains "team_id": 42, the output will contain both "team_id": 42 // and "fleet_id": 42. // +// By default a renamed key produces a clean split: the old-named key keeps an +// all-old subtree and the new-named key gets an all-new subtree (via +// RewriteOldToNewKeys), so each subtree is internally single-named. A renamed +// leaf at the top level (or under a non-renamed key) is instead duplicated in +// place, so both names appear as siblings with the same value. +// +// A rule with Inline set opts into "merged" duplication for that container: its +// old-named subtree additionally carries the new-named copies of any nested +// renamed containers, so both names appear together on the same object (e.g. +// "abm_tokens" holding both "macos_team" and "macos_fleet"). Leaf renames +// inside an inlined subtree are still kept single-named per container (so +// "macos_team" holds "team_id" while its sibling "macos_fleet" holds +// "fleet_id") rather than cross-contaminating both id names into one object. +// // If the new key already exists in the same object scope, the duplication is // skipped for that key (to avoid producing duplicate keys when the source // struct already has both, or when the function is called more than once). @@ -29,21 +43,40 @@ type DuplicateJSONKeysOpts struct { // library. Duplicates are deferred until the closing '}' of each object so // that naturally-occurring new keys can be detected and skipped. func DuplicateJSONKeys(data []byte, rules []AliasRule, opts ...DuplicateJSONKeysOpts) []byte { + compact := len(opts) > 0 && opts[0].Compact + return duplicateJSONKeys(data, rules, compact) +} + +// duplicateJSONKeys is the recursive core of DuplicateJSONKeys. +// +// An Inline container is the only recursive case: its old-named subtree is +// re-run through this function so nested renames surface there too — exactly as +// they did before the container itself was renamed. That recursion needs no +// special mode because the default rules already produce the right shape: +// nested renamed *containers* split cleanly into old/new siblings (their values +// are consumed whole by ReadValue, so their leaves are never duplicated in +// place), while nested renamed *leaves* are duplicated in place. The new-named +// subtree is always a clean RewriteOldToNewKeys copy. +func duplicateJSONKeys(data []byte, rules []AliasRule, compact bool) []byte { if len(rules) == 0 || len(data) == 0 { return data } oldToNew := make(map[string]string, len(rules)) newToOld := make(map[string]string, len(rules)) + inlineOld := make(map[string]bool, len(rules)) for _, r := range rules { oldToNew[r.OldKey] = r.NewKey newToOld[r.NewKey] = r.OldKey + if r.Inline { + inlineOld[r.OldKey] = true + } } var buf bytes.Buffer dec := jsontext.NewDecoder(bytes.NewReader(data), jsontext.AllowDuplicateNames(true)) encOpts := []jsontext.Options{jsontext.AllowDuplicateNames(true)} - if len(opts) == 0 || !opts[0].Compact { + if !compact { encOpts = append(encOpts, jsontext.WithIndent(" ")) } enc := jsontext.NewEncoder(&buf, encOpts...) @@ -137,21 +170,29 @@ func DuplicateJSONKeys(data []byte, rules []AliasRule, opts ...DuplicateJSONKeys return data } - // Write the original value as-is for the old key — it - // already uses old names from json.Marshal, so no - // transformation is needed. - if err := enc.WriteValue(val); err != nil { + // Old-named subtree. By default it is written as-is (the + // value already uses old names from json.Marshal). An Inline + // container instead re-runs the duplicator over its value so + // nested renames also surface under the old name, the way + // they did before this container was renamed. + if inlineOld[keyName] && startsWithContainer(val) { + // compact is irrelevant here: the result is re-encoded + // by the outer encoder, which applies its own indent. + oldVal := duplicateJSONKeys([]byte(val), rules, true) + if err := enc.WriteValue(jsontext.Value(oldVal)); err != nil { + return data + } + } else if err := enc.WriteValue(val); err != nil { return data } - // For the new key, rename nested keys to new names only - // (removing old names) so the new-name subtree is clean. + // New-named sibling: a clean, fully new-named copy. For a + // scalar this is the same value, which yields an in-place + // duplicate (both old and new key on the same object). newVal, renameErr := RewriteOldToNewKeys([]byte(val), rules) if renameErr != nil { newVal = []byte(val) // fall back to original value on error } - - // Defer the duplicate for emission at '}'. if len(scopes) > 0 { scopes[len(scopes)-1].pending = append( scopes[len(scopes)-1].pending, @@ -179,3 +220,19 @@ func DuplicateJSONKeys(data []byte, rules []AliasRule, opts ...DuplicateJSONKeys return buf.Bytes() } + +// startsWithContainer reports whether the JSON value v is an object or array +// (as opposed to a scalar: string, number, bool, or null). +func startsWithContainer(v []byte) bool { + for _, b := range v { + switch b { + case ' ', '\t', '\n', '\r': + continue + case '{', '[': + return true + default: + return false + } + } + return false +} diff --git a/server/platform/endpointer/json_key_duplicator_test.go b/server/platform/endpointer/json_key_duplicator_test.go index f47d1169f89..c95d447deff 100644 --- a/server/platform/endpointer/json_key_duplicator_test.go +++ b/server/platform/endpointer/json_key_duplicator_test.go @@ -181,6 +181,87 @@ func TestDuplicateJSONKeys(t *testing.T) { assert.False(t, hasOldKey, "new-name container should not have old child key") }, }, + { + // Three-level rename matching the ABM tokens response: + // abm_tokens→ab_tokens wraps an array of objects whose + // macos_team→macos_fleet containers in turn hold team_id→fleet_id. + // The previous release returned both the old- and new-named + // containers (with clean, internally-consistent leaves) on the same + // object under abm_tokens; the duplicator must reproduce that while + // adding the new top-level ab_tokens key. + name: "MultiLevelRenamedContainers", + input: `{"abm_tokens":[{"id":1,` + + `"macos_team":{"name":"T","team_id":22},` + + `"ios_team":{"name":"T","team_id":22}}]}`, + rules: []AliasRule{ + {OldKey: "abm_tokens", NewKey: "ab_tokens", Inline: true}, + {OldKey: "macos_team", NewKey: "macos_fleet"}, + {OldKey: "ios_team", NewKey: "ios_fleet"}, + {OldKey: "team_id", NewKey: "fleet_id"}, + }, + validate: func(t *testing.T, result []byte) { + assert.True(t, json.Valid(result), "result should be valid JSON: %s", string(result)) + var m map[string]any + require.NoError(t, json.Unmarshal(result, &m)) + + // The old top-level key carries BOTH container variants on the + // same token object, each with clean leaves (no cross id). + abm := m["abm_tokens"].([]any) + require.Len(t, abm, 1) + tok := abm[0].(map[string]any) + macosTeam := tok["macos_team"].(map[string]any) + assert.Equal(t, float64(22), macosTeam["team_id"]) + _, hasFleetID := macosTeam["fleet_id"] + assert.False(t, hasFleetID, "macos_team must not be contaminated with fleet_id") + macosFleet := tok["macos_fleet"].(map[string]any) + assert.Equal(t, float64(22), macosFleet["fleet_id"]) + _, hasTeamID := macosFleet["team_id"] + assert.False(t, hasTeamID, "macos_fleet must not be contaminated with team_id") + + // The new top-level key is a clean, fully new-named copy. + ab := m["ab_tokens"].([]any) + require.Len(t, ab, 1) + newTok := ab[0].(map[string]any) + _, hasOldContainer := newTok["macos_team"] + assert.False(t, hasOldContainer, "ab_tokens token should not contain old-named macos_team") + newFleet := newTok["macos_fleet"].(map[string]any) + assert.Equal(t, float64(22), newFleet["fleet_id"]) + }, + }, + { + // Inline container whose nested renames are LEAVES, not containers + // (matching the apple_business_manager response, where macos_team is + // a plain string). The previous release duplicated those leaves in + // place under the (then-unrenamed) apple_business_manager key, so the + // inlined old key must carry both leaf names on the same object. + name: "InlineContainerWithLeafChildren", + input: `{"apple_business_manager":[` + + `{"organization_name":"X","macos_team":"T"}]}`, + rules: []AliasRule{ + {OldKey: "apple_business_manager", NewKey: "apple_business", Inline: true}, + {OldKey: "macos_team", NewKey: "macos_fleet"}, + }, + validate: func(t *testing.T, result []byte) { + assert.True(t, json.Valid(result), "result should be valid JSON: %s", string(result)) + var m map[string]any + require.NoError(t, json.Unmarshal(result, &m)) + + // Old key: both leaf names duplicated in place on the same object. + old := m["apple_business_manager"].([]any) + require.Len(t, old, 1) + item := old[0].(map[string]any) + assert.Equal(t, "T", item["macos_team"]) + assert.Equal(t, "T", item["macos_fleet"]) + + // New key: clean new-named copy only. + abNew := m["apple_business"].([]any) + require.Len(t, abNew, 1) + newItem := abNew[0].(map[string]any) + assert.Equal(t, "T", newItem["macos_fleet"]) + _, hasOld := newItem["macos_team"] + assert.False(t, hasOld, "apple_business item should not contain old-named macos_team") + }, + }, { name: "ArrayOfObjects", input: `[{"team_id": 1}, {"team_id": 2}]`, diff --git a/server/platform/endpointer/json_key_rewriter.go b/server/platform/endpointer/json_key_rewriter.go index 4eae18c8d5c..4b2b59fa749 100644 --- a/server/platform/endpointer/json_key_rewriter.go +++ b/server/platform/endpointer/json_key_rewriter.go @@ -28,6 +28,15 @@ func (e *AliasConflictError) Error() string { type AliasRule struct { OldKey string NewKey string + // Inline opts a renamed container into "merged" response duplication: + // instead of the default clean split (the old key holds an all-old subtree + // and the new key an all-new one), the old key's subtree also carries the + // new-named copies of any nested renamed containers — so both names appear + // together on the same object. Set via the `,inline` option on the + // `renameto` struct tag (e.g. `renameto:"ab_tokens,inline"`). It only + // affects response encoding (DuplicateJSONKeys); request decoding ignores + // it. + Inline bool } // JSONKeyRewriteReader is a streaming io.Reader that handles @@ -153,6 +162,8 @@ func RewriteDeprecatedKeys(data []byte, rules []AliasRule) ([]byte, map[string]s func RewriteOldToNewKeys(data []byte, rules []AliasRule) ([]byte, error) { reversed := make([]AliasRule, len(rules)) for i, r := range rules { + // Inline is intentionally not preserved: this only renames keys, it + // never duplicates them. reversed[i] = AliasRule{OldKey: r.NewKey, NewKey: r.OldKey} } result, _, err := RewriteDeprecatedKeys(data, reversed) diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index e7c00f00e9c..99f04ea616e 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -6884,7 +6884,7 @@ func (uploadABMTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) } type uploadABMTokenResponse struct { - Token *fleet.ABMToken `json:"abm_token,omitempty" renameto:"ab_token"` + Token *fleet.ABMToken `json:"abm_token,omitempty" renameto:"ab_token,inline"` Err error `json:"error,omitempty"` } @@ -6954,7 +6954,7 @@ func (svc *Service) DeleteABMToken(ctx context.Context, tokenID uint) error { type listABMTokensResponse struct { Err error `json:"error,omitempty"` - Tokens []*fleet.ABMToken `json:"abm_tokens" renameto:"ab_tokens"` + Tokens []*fleet.ABMToken `json:"abm_tokens" renameto:"ab_tokens,inline"` } func (r listABMTokensResponse) Error() error { return r.Err } @@ -7021,7 +7021,7 @@ type updateABMTokenTeamsRequest struct { } type updateABMTokenTeamsResponse struct { - ABMToken *fleet.ABMToken `json:"abm_token,omitempty" renameto:"ab_token"` + ABMToken *fleet.ABMToken `json:"abm_token,omitempty" renameto:"ab_token,inline"` Err error `json:"error,omitempty"` } @@ -7084,7 +7084,7 @@ func (renewABMTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) } type renewABMTokenResponse struct { - ABMToken *fleet.ABMToken `json:"abm_token,omitempty" renameto:"ab_token"` + ABMToken *fleet.ABMToken `json:"abm_token,omitempty" renameto:"ab_token,inline"` Err error `json:"error,omitempty"` } diff --git a/server/service/client_mdm.go b/server/service/client_mdm.go index 4155062be01..f3d9352f553 100644 --- a/server/service/client_mdm.go +++ b/server/service/client_mdm.go @@ -67,7 +67,7 @@ func (c *Client) RequestAppleCSR() ([]byte, error) { // RequestAppleABM requests a signed CSR from the Fleet server and returns the // public key bytes func (c *Client) RequestAppleABM() ([]byte, error) { - verb, path := "GET", "/api/latest/fleet/mdm/apple/abm_public_key" + verb, path := "GET", "/api/latest/fleet/mdm/apple/ab_public_key" var resp generateABMKeyPairResponse err := c.authenticatedRequest(nil, verb, path, &resp) return resp.PublicKey, err diff --git a/server/service/handler.go b/server/service/handler.go index 42d3a19a873..681eb424533 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -836,7 +836,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // Deprecated: this endpoint shouldn't be used anymore in favor of the // new flow described in https://github.com/fleetdm/fleet/issues/10383 ue.POST("/api/_version_/fleet/mdm/apple/dep/key_pair", newMDMAppleDEPKeyPairEndpoint, nil) - ue.GET("/api/_version_/fleet/mdm/apple/abm_public_key", generateABMKeyPairEndpoint, nil) + ue.GET("/api/_version_/fleet/mdm/apple/ab_public_key", generateABMKeyPairEndpoint, nil) ue.POST("/api/_version_/fleet/ab_tokens", uploadABMTokenEndpoint, uploadABMTokenRequest{}) ue.DELETE("/api/_version_/fleet/ab_tokens/{id:[0-9]+}", deleteABMTokenEndpoint, deleteABMTokenRequest{}) ue.GET("/api/_version_/fleet/ab_tokens", listABMTokensEndpoint, nil) diff --git a/server/service/handler_deprecated_paths.go b/server/service/handler_deprecated_paths.go index b287730ec12..d2ec988bfe9 100644 --- a/server/service/handler_deprecated_paths.go +++ b/server/service/handler_deprecated_paths.go @@ -246,4 +246,8 @@ var deprecatedPathAliases = []eu.DeprecatedPathAlias{ Method: "PATCH", PrimaryPath: "/api/_version_/fleet/ab_tokens/{id:[0-9]+}/renew", DeprecatedPaths: []string{"/api/_version_/fleet/abm_tokens/{id:[0-9]+}/renew"}, }, + { + Method: "GET", PrimaryPath: "/api/_version_/fleet/mdm/apple/ab_public_key", + DeprecatedPaths: []string{"/api/_version_/fleet/mdm/apple/abm_public_key"}, + }, } diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 6d72ff30681..801f17516a7 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -11956,16 +11956,22 @@ func (s *integrationMDMTestSuite) TestABMAssetManagement() { testSetEmptyPrivateKey = true t.Cleanup(func() { testSetEmptyPrivateKey = false }) - r := s.Do("GET", "/api/latest/fleet/mdm/apple/abm_public_key", generateABMKeyPairResponse{}, http.StatusInternalServerError) + r := s.Do("GET", "/api/latest/fleet/mdm/apple/ab_public_key", generateABMKeyPairResponse{}, http.StatusInternalServerError) require.Contains(t, extractServerErrorText(r.Body), "Couldn't download public key. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") testSetEmptyPrivateKey = false // grab the current public key var abmResp generateABMKeyPairResponse - s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &abmResp) + s.DoJSON("GET", "/api/latest/fleet/mdm/apple/ab_public_key", nil, http.StatusOK, &abmResp) require.Nil(t, abmResp.Err) require.NotEmpty(t, abmResp.PublicKey) + // the deprecated abm_public_key path still resolves to the same endpoint + var deprecatedResp generateABMKeyPairResponse + s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &deprecatedResp) + require.Nil(t, deprecatedResp.Err) + require.NotEmpty(t, deprecatedResp.PublicKey) + var tokensResp listABMTokensResponse s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp) tok := s.getABMTokenByName(t.Name(), tokensResp.Tokens) @@ -11982,7 +11988,7 @@ func (s *integrationMDMTestSuite) TestABMAssetManagement() { // enable ABM again var newABMResp generateABMKeyPairResponse - s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &newABMResp) + s.DoJSON("GET", "/api/latest/fleet/mdm/apple/ab_public_key", nil, http.StatusOK, &newABMResp) require.Nil(t, newABMResp.Err) require.NotEmpty(t, newABMResp.PublicKey) block, _ := pem.Decode(newABMResp.PublicKey) @@ -11991,7 +11997,7 @@ func (s *integrationMDMTestSuite) TestABMAssetManagement() { // we should always return the same values to support renewing the token var renewABMResp generateABMKeyPairResponse - s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &renewABMResp) + s.DoJSON("GET", "/api/latest/fleet/mdm/apple/ab_public_key", nil, http.StatusOK, &renewABMResp) require.Nil(t, renewABMResp.Err) require.NotEmpty(t, renewABMResp.PublicKey) require.Equal(t, renewABMResp.PublicKey, newABMResp.PublicKey) @@ -12003,7 +12009,7 @@ func (s *integrationMDMTestSuite) TestABMAssetManagement() { func (s *integrationMDMTestSuite) enableABM(orgName string) *fleet.ABMToken { t := s.T() var abmResp generateABMKeyPairResponse - s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &abmResp) + s.DoJSON("GET", "/api/latest/fleet/mdm/apple/ab_public_key", nil, http.StatusOK, &abmResp) require.Nil(t, abmResp.Err) require.NotEmpty(t, abmResp.PublicKey) block, _ := pem.Decode(abmResp.PublicKey) From 52b2b01915ad38189abdbf2dea647b8dffe1a695 Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Tue, 2 Jun 2026 14:51:22 -0400 Subject: [PATCH 3/7] Fix issues with renameto inline tag and gitops --- cmd/fleetctl/fleetctl/generate_gitops.go | 7 +++++-- frontend/interfaces/mdm.ts | 11 ++++++++--- frontend/interfaces/team.ts | 7 ++++++- .../AppleBusinessManagerTableConfig.tsx | 8 ++++---- .../EditTeamsAbmModal/EditTeamsAbmModal.tsx | 12 ++++++------ pkg/spec/gitops_validate.go | 5 +++-- pkg/spec/gitops_validate_test.go | 7 +++++++ 7 files changed, 39 insertions(+), 18 deletions(-) diff --git a/cmd/fleetctl/fleetctl/generate_gitops.go b/cmd/fleetctl/fleetctl/generate_gitops.go index 0d7c47d7bc5..f0281d51fe0 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops.go +++ b/cmd/fleetctl/fleetctl/generate_gitops.go @@ -105,9 +105,12 @@ func jsonFieldName(t reflect.Type, fieldName string) string { panic(fieldName + " not found in " + t.Name()) } - // Prefer the renameto tag (new canonical name) if it exists. + // Prefer the renameto tag (new canonical name) if it exists, stripping any + // options like ",inline". if renameTo := field.Tag.Get("renameto"); renameTo != "" { - return renameTo + if name, _, _ := strings.Cut(renameTo, ","); name != "" { + return name + } } tag := field.Tag.Get("json") diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index 9a4814f0033..26630bf151f 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -21,6 +21,11 @@ export type ITokenTeam = { name: string; }; +export type ITokenFleet = { + fleet_id: number; + name: string; +}; + export interface IMdmAbToken { id: number; apple_id: string; @@ -28,9 +33,9 @@ export interface IMdmAbToken { mdm_server_url: string; renew_date: string; terms_expired: boolean; - macos_team: ITokenTeam; - ios_team: ITokenTeam; - ipados_team: ITokenTeam; + macos_fleet: ITokenFleet; + ios_fleet: ITokenFleet; + ipados_fleet: ITokenFleet; } export interface IMdmVppToken { diff --git a/frontend/interfaces/team.ts b/frontend/interfaces/team.ts index 4163faa9235..70012a4217e 100644 --- a/frontend/interfaces/team.ts +++ b/frontend/interfaces/team.ts @@ -7,7 +7,7 @@ import { import enrollSecretInterface, { IEnrollSecret } from "./enroll_secret"; import { ITeamIntegrations } from "./integration"; import { UserRole } from "./user"; -import { ITokenTeam } from "./mdm"; +import { ITokenFleet, ITokenTeam } from "./mdm"; export default PropTypes.shape({ id: PropTypes.number.isRequired, @@ -153,3 +153,8 @@ export const getTeamDisplayName = (team: ITokenTeam) => team.team_id === APP_CONTEXT_NO_TEAM_ID ? APP_CONTEXT_NO_TEAM_SUMMARY.name : team.name; + +export const getFleetDisplayName = (fleet: ITokenFleet) => + fleet.fleet_id === APP_CONTEXT_NO_TEAM_ID + ? APP_CONTEXT_NO_TEAM_SUMMARY.name + : fleet.name; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx index 485706ba7fe..6396822d078 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx @@ -3,7 +3,7 @@ import { CellProps, Column } from "react-table"; import { IMdmAbToken } from "interfaces/mdm"; import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config"; -import { getTeamDisplayName } from "interfaces/team"; +import { getFleetDisplayName } from "interfaces/team"; import { IDropdownOption } from "interfaces/dropdownOption"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; @@ -112,7 +112,7 @@ export const generateTableConfig = ( }, { id: "macos_team", - accessor: (originalRow) => getTeamDisplayName(originalRow.macos_team), + accessor: (originalRow) => getFleetDisplayName(originalRow.macos_fleet), Header: () => { const titleWithToolTip = ( getTeamDisplayName(originalRow.ios_team), + accessor: (originalRow) => getFleetDisplayName(originalRow.ios_fleet), Header: () => { const titleWithToolTip = ( getTeamDisplayName(originalRow.ipados_team), + accessor: (originalRow) => getFleetDisplayName(originalRow.ipados_fleet), Header: () => { const titleWithToolTip = ( { * returned by the get token API. */ interface SelectedTeamNames { - ios_team: IMdmAbToken["ios_team"]["name"]; - ipados_team: IMdmAbToken["ipados_team"]["name"]; - macos_team: IMdmAbToken["macos_team"]["name"]; + ios_team: IMdmAbToken["ios_fleet"]["name"]; + ipados_team: IMdmAbToken["ipados_fleet"]["name"]; + macos_team: IMdmAbToken["macos_fleet"]["name"]; } /** @@ -82,9 +82,9 @@ const EditTeamsAbmModal = ({ const [selectedTeamNames, setSelectedTeamNames] = useState( { - ios_team: token.ios_team.name, - ipados_team: token.ipados_team.name, - macos_team: token.macos_team.name, + ios_team: token.ios_fleet.name, + ipados_team: token.ipados_fleet.name, + macos_team: token.macos_fleet.name, } ); diff --git a/pkg/spec/gitops_validate.go b/pkg/spec/gitops_validate.go index 7f57d3b7e9e..06d2b7372e8 100644 --- a/pkg/spec/gitops_validate.go +++ b/pkg/spec/gitops_validate.go @@ -112,8 +112,9 @@ func collectFields(t reflect.Type, keys map[string]fieldInfo) { } // Also register the "renameto" alias (deprecated field name mappings) - // so that both old and new names are accepted. - if alias := field.Tag.Get("renameto"); alias != "" { + // so that both old and new names are accepted. Strip any options like + // ",inline" so the registered key is just the new name. + if alias := strings.Split(field.Tag.Get("renameto"), ",")[0]; alias != "" { keys[alias] = fieldInfo{ jsonName: alias, typ: field.Type, diff --git a/pkg/spec/gitops_validate_test.go b/pkg/spec/gitops_validate_test.go index 4bf39946a6c..0263f11a119 100644 --- a/pkg/spec/gitops_validate_test.go +++ b/pkg/spec/gitops_validate_test.go @@ -78,6 +78,13 @@ func TestKnownJSONKeys(t *testing.T) { keys = knownJSONKeys(reflect.TypeFor[fleet.LabelSpec]()) assert.Contains(t, keys, "team_id") assert.Contains(t, keys, "fleet_id") + + // MDM has `json:"apple_business_manager" renameto:"apple_business,inline"` + // — the registered alias must drop the ",inline" option. + keys = knownJSONKeys(reflect.TypeFor[fleet.MDM]()) + assert.Contains(t, keys, "apple_business_manager") + assert.Contains(t, keys, "apple_business") + assert.NotContains(t, keys, "apple_business,inline") }) t.Run("caching works", func(t *testing.T) { From 22fa87584689d47d10583dd0ce939af8157bd18a Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Tue, 2 Jun 2026 16:02:21 -0400 Subject: [PATCH 4/7] Fix lint/test failures --- cmd/fleetctl/fleetctl/get.go | 7 +++---- .../VppPage/components/VppTable/VppTableConfig.tsx | 4 ++-- pkg/spec/gitops_validate.go | 2 +- server/platform/endpointer/extract_alias_rules_test.go | 2 +- server/platform/endpointer/json_key_duplicator.go | 6 +++--- server/platform/endpointer/json_key_duplicator_test.go | 6 +++--- server/service/integration_mdm_test.go | 2 +- 7 files changed, 14 insertions(+), 15 deletions(-) diff --git a/cmd/fleetctl/fleetctl/get.go b/cmd/fleetctl/fleetctl/get.go index 7753cd528a9..67b13b606f9 100644 --- a/cmd/fleetctl/fleetctl/get.go +++ b/cmd/fleetctl/fleetctl/get.go @@ -1678,8 +1678,7 @@ func runGetMDMAB(c *cli.Context) error { bm, err := client.GetAppleBM() if err != nil { - var nfe service.NotFoundErr - if errors.As(err, &nfe) { + if _, ok := errors.AsType[service.NotFoundErr](err); ok { log(c, "Error: No Apple Business (AB) server token found. Use `fleetctl generate mdm-ab` and then `fleet serve` with `mdm` configuration to automatically enroll macOS hosts to Fleet.\n") return nil } @@ -1688,14 +1687,14 @@ func runGetMDMAB(c *cli.Context) error { defaultTeam := bm.DefaultTeam if defaultTeam == "" { - defaultTeam = "No team" + defaultTeam = "Unassigned" } printKeyValueTable(c, [][]string{ {"Apple ID:", bm.AppleID}, {"Organization name:", bm.OrgName}, {"MDM server URL:", bm.MDMServerURL}, {"Renew date:", bm.RenewDate.Format("January 2, 2006")}, - {"Default team:", defaultTeam}, + {"Default fleet:", defaultTeam}, }) warnDate := time.Now().Add(expirationWarning) diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx index 7304097fc6d..60230116910 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx @@ -1,7 +1,7 @@ import React from "react"; import { CellProps, Column } from "react-table"; -import { IMdmAbToken, IMdmVppToken } from "interfaces/mdm"; +import { IMdmVppToken } from "interfaces/mdm"; import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config"; import { IDropdownOption } from "interfaces/dropdownOption"; @@ -148,6 +148,6 @@ export const generateTableConfig = ( ]; }; -export const generateTableData = (data: IMdmAbToken[]) => { +export const generateTableData = (data: IMdmVppToken[]) => { return data; }; diff --git a/pkg/spec/gitops_validate.go b/pkg/spec/gitops_validate.go index 06d2b7372e8..f9fdfb52b9c 100644 --- a/pkg/spec/gitops_validate.go +++ b/pkg/spec/gitops_validate.go @@ -114,7 +114,7 @@ func collectFields(t reflect.Type, keys map[string]fieldInfo) { // Also register the "renameto" alias (deprecated field name mappings) // so that both old and new names are accepted. Strip any options like // ",inline" so the registered key is just the new name. - if alias := strings.Split(field.Tag.Get("renameto"), ",")[0]; alias != "" { + if alias, _, _ := strings.Cut(field.Tag.Get("renameto"), ","); alias != "" { keys[alias] = fieldInfo{ jsonName: alias, typ: field.Type, diff --git a/server/platform/endpointer/extract_alias_rules_test.go b/server/platform/endpointer/extract_alias_rules_test.go index c517c0d58b8..37f04492286 100644 --- a/server/platform/endpointer/extract_alias_rules_test.go +++ b/server/platform/endpointer/extract_alias_rules_test.go @@ -59,7 +59,7 @@ func (s *extractAliasRulesSuite) TestRenametoInlineOption() { TeamID uint `json:"team_id" renameto:"fleet_id"` } rules := ExtractAliasRules(inlineAlias{}) - require.Equal(s.T(), []AliasRule{ + s.Require().Equal([]AliasRule{ {OldKey: "abm_tokens", NewKey: "ab_tokens", Inline: true}, {OldKey: "team_id", NewKey: "fleet_id"}, }, rules) diff --git a/server/platform/endpointer/json_key_duplicator.go b/server/platform/endpointer/json_key_duplicator.go index ec09c1fbd8f..cbbeebe259e 100644 --- a/server/platform/endpointer/json_key_duplicator.go +++ b/server/platform/endpointer/json_key_duplicator.go @@ -64,12 +64,12 @@ func duplicateJSONKeys(data []byte, rules []AliasRule, compact bool) []byte { oldToNew := make(map[string]string, len(rules)) newToOld := make(map[string]string, len(rules)) - inlineOld := make(map[string]bool, len(rules)) + inlineOld := make(map[string]struct{}, len(rules)) for _, r := range rules { oldToNew[r.OldKey] = r.NewKey newToOld[r.NewKey] = r.OldKey if r.Inline { - inlineOld[r.OldKey] = true + inlineOld[r.OldKey] = struct{}{} } } @@ -175,7 +175,7 @@ func duplicateJSONKeys(data []byte, rules []AliasRule, compact bool) []byte { // container instead re-runs the duplicator over its value so // nested renames also surface under the old name, the way // they did before this container was renamed. - if inlineOld[keyName] && startsWithContainer(val) { + if _, ok := inlineOld[keyName]; ok && startsWithContainer(val) { // compact is irrelevant here: the result is re-encoded // by the outer encoder, which applies its own indent. oldVal := duplicateJSONKeys([]byte(val), rules, true) diff --git a/server/platform/endpointer/json_key_duplicator_test.go b/server/platform/endpointer/json_key_duplicator_test.go index c95d447deff..985378a2916 100644 --- a/server/platform/endpointer/json_key_duplicator_test.go +++ b/server/platform/endpointer/json_key_duplicator_test.go @@ -210,11 +210,11 @@ func TestDuplicateJSONKeys(t *testing.T) { require.Len(t, abm, 1) tok := abm[0].(map[string]any) macosTeam := tok["macos_team"].(map[string]any) - assert.Equal(t, float64(22), macosTeam["team_id"]) + assert.InDelta(t, float64(22), macosTeam["team_id"], 0) _, hasFleetID := macosTeam["fleet_id"] assert.False(t, hasFleetID, "macos_team must not be contaminated with fleet_id") macosFleet := tok["macos_fleet"].(map[string]any) - assert.Equal(t, float64(22), macosFleet["fleet_id"]) + assert.InDelta(t, float64(22), macosFleet["fleet_id"], 0) _, hasTeamID := macosFleet["team_id"] assert.False(t, hasTeamID, "macos_fleet must not be contaminated with team_id") @@ -225,7 +225,7 @@ func TestDuplicateJSONKeys(t *testing.T) { _, hasOldContainer := newTok["macos_team"] assert.False(t, hasOldContainer, "ab_tokens token should not contain old-named macos_team") newFleet := newTok["macos_fleet"].(map[string]any) - assert.Equal(t, float64(22), newFleet["fleet_id"]) + assert.InDelta(t, float64(22), newFleet["fleet_id"], 0) }, }, { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 82c0fa6593a..d46dc3ab6da 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -11992,7 +11992,7 @@ func (s *integrationMDMTestSuite) TestABMAssetManagement() { // the deprecated abm_public_key path still resolves to the same endpoint var deprecatedResp generateABMKeyPairResponse s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &deprecatedResp) - require.Nil(t, deprecatedResp.Err) + require.NoError(t, deprecatedResp.Err) require.NotEmpty(t, deprecatedResp.PublicKey) var tokensResp listABMTokensResponse From 9f3cafcc7b3a9e5b38d3227b196d32070adb2eb7 Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Tue, 2 Jun 2026 16:05:28 -0400 Subject: [PATCH 5/7] Fix a couple typos --- server/fleet/app.go | 2 +- server/fleet/apple_mdm.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/fleet/app.go b/server/fleet/app.go index 97b8f5e4bd0..d4e81a5f3bd 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -188,7 +188,7 @@ type MDM struct { // If not set, the server will use Fleet server URL (recommended). AppleServerURL string `json:"apple_server_url"` - // Deprecated: use AppleBussinessManager instead + // Deprecated: use AppleBusinessManager instead DeprecatedAppleBMDefaultTeam string `json:"apple_bm_default_team,omitempty"` //nolint:apiparamcheck // not renaming already-deprecated field // AppleBusinessManager defines the associations between AB tokens diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index bb3aa316e3d..b38d21393c0 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -728,10 +728,10 @@ type HostDEPAssignment struct { // HostID is the id of the host in Fleet. HostID uint `db:"host_id" json:"-"` // AddedAt is the timestamp when Fleet was notified that device was added to the Fleet MDM - // server in Apple Busines Manager (AB). + // server in Apple Business (AB). AddedAt time.Time `db:"added_at" json:"added_at"` // DeletedAt is the timestamp when Fleet was notified that device was deleted from the Fleet - // MDM server in Apple Busines Manager (AB). + // MDM server in Apple Business (AB). DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"` // ABMTokenID is the ID of the AB token that was used to make this DEP assignment. ABMTokenID *uint `db:"abm_token_id" json:"abm_token_id" renameto:"ab_token_id"` From 8dc8ff0c39db927c16a3aa3b7d69f67b7bc0cd51 Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Wed, 3 Jun 2026 08:43:19 -0400 Subject: [PATCH 6/7] Fix test --- cmd/fleetctl/fleetctl/get_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/fleetctl/fleetctl/get_test.go b/cmd/fleetctl/fleetctl/get_test.go index 515d8cb4723..65b4f5d0c32 100644 --- a/cmd/fleetctl/fleetctl/get_test.go +++ b/cmd/fleetctl/fleetctl/get_test.go @@ -2585,7 +2585,7 @@ func TestGetAppleBM(t *testing.T) { assert.Contains(t, out, "Organization name:") assert.Contains(t, out, "MDM server URL:") assert.Contains(t, out, "Renew date:") - assert.Contains(t, out, "Default team:") + assert.Contains(t, out, "Default fleet:") }) t.Run("premium license, no token", func(t *testing.T) { From 2384ef65feb2736087a9f68865d8987b7c31e49e Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Wed, 3 Jun 2026 13:18:00 -0400 Subject: [PATCH 7/7] Update api_endpoints.yml --- server/api_endpoints/api_endpoints.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/api_endpoints/api_endpoints.yml b/server/api_endpoints/api_endpoints.yml index 0247698824f..d554fc698c6 100644 --- a/server/api_endpoints/api_endpoints.yml +++ b/server/api_endpoints/api_endpoints.yml @@ -327,9 +327,13 @@ - method: "GET" path: "/api/v1/fleet/apns" display_name: "Get APNs certificate" +- method: "GET" + path: "/api/v1/fleet/ab_tokens" + display_name: "List Apple Business (AB) tokens" - method: "GET" path: "/api/v1/fleet/abm_tokens" - display_name: "List Apple Business Manager (ABM) tokens" + display_name: "List Apple Business (AB) tokens" + deprecated: true - method: "GET" path: "/api/v1/fleet/vpp_tokens" display_name: "List Volume Purchasing Program (VPP) tokens" @@ -526,9 +530,13 @@ - method: "POST" path: "/api/v1/fleet/spec/labels" display_name: "Apply labels spec" +- method: "GET" + path: "/api/v1/fleet/ab_tokens/count" + display_name: "Count Apple Business (AB) tokens" - method: "GET" path: "/api/v1/fleet/abm_tokens/count" - display_name: "Count Apple Business Manager (ABM) tokens" + display_name: "Count Apple Business (AB) tokens" + deprecated: true - method: "GET" path: "/api/v1/fleet/spec/certificate_authorities" display_name: "Get certificate authorities spec"