diff --git a/changes/42508-rename-abm-to-ab b/changes/42508-rename-abm-to-ab new file mode 100644 index 00000000000..1ad3c0bf443 --- /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` 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/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.go b/cmd/fleetctl/fleetctl/generate_gitops.go index 9e5cbe9a8db..7c80c3bcc95 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/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 a1b7afb43a6..67b13b606f9 100644 --- a/cmd/fleetctl/fleetctl/get.go +++ b/cmd/fleetctl/fleetctl/get.go @@ -19,6 +19,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" @@ -494,6 +495,7 @@ func getCommand() *cli.Command { getFleetsCommand(), getSoftwareCommand(), getMDMAppleCommand(), + getMDMABCommand(), getMDMAppleBMCommand(), getMDMCommandResultsCommand(), getMDMCommandsCommand(), @@ -1633,58 +1635,78 @@ 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 { + 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 - }, + } + return fmt.Errorf("could not get Apple Business information: %w", err) + } + + defaultTeam := bm.DefaultTeam + if defaultTeam == "" { + 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 fleet:", 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 daa67939b53..65b4f5d0c32 100644 --- a/cmd/fleetctl/fleetctl/get_test.go +++ b/cmd/fleetctl/fleetctl/get_test.go @@ -2565,7 +2565,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) @@ -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) { @@ -2596,7 +2596,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) { @@ -2610,7 +2610,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 676aa041f6a..de59a34ccec 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) @@ -1130,13 +1133,13 @@ func applyABMTokenAssignmentIfNeeded( continue } if _, ok := knownTeams[norm.NFC.String(abmTeam)]; !ok { - 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 19a6ac0277d..66bf76850dc 100644 --- a/cmd/fleetctl/fleetctl/gitops_test.go +++ b/cmd/fleetctl/fleetctl/gitops_test.go @@ -3967,10 +3967,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 3037d1e2c34..77563ba9bad 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 cff329a99db..9e328844dbf 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 c5871ec30d7..85be72a99b9 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 3364e636bfe..a8e48b35a30 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 13778ab66dc..4db02fb2f19 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 d3df15ad85c..6bb8e09e0d8 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 5867b8d9ea5..be26420c29d 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 bd9a3bfe328..f9b8db26d58 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 3396258fe08..a3f030b4c5f 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 a2e61ba19c4..51ed634bd4d 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -21,16 +21,21 @@ export type ITokenTeam = { name: string; }; -export interface IMdmAbmToken { +export type ITokenFleet = { + fleet_id: number; + name: string; +}; + +export interface IMdmAbToken { id: number; apple_id: string; org_name: string; 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 0e46a906165..5d49e8207a4 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 { EndUserLocalAccountType, ITokenTeam } from "./mdm"; +import { EndUserLocalAccountType, ITokenFleet, ITokenTeam } from "./mdm"; export default PropTypes.shape({ id: PropTypes.number.isRequired, @@ -154,3 +154,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/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..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 @@ -1,9 +1,9 @@ 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 { getFleetDisplayName } from "interfaces/team"; import { IDropdownOption } from "interfaces/dropdownOption"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; @@ -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[] => { @@ -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 = ( { +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..7a954ee104d 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_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/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..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 { IMdmAbmToken, 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: IMdmAbmToken[]) => { +export const generateTableData = (data: IMdmVppToken[]) => { return data; }; diff --git a/frontend/services/entities/mdm_apple_bm.ts b/frontend/services/entities/mdm_apple_bm.ts index 604eba465b7..13ca2779cf7 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 { @@ -51,21 +51,21 @@ 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 => { - 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..7c4c73fe2fd 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -144,13 +144,13 @@ 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_APPLE_ABM_PUBLIC_KEY: `/${API_VERSION}/fleet/mdm/apple/abm_public_key`, + 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_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/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 0b4ba963e55..d0b5e343cb6 100644 --- a/pkg/spec/gitops_test.go +++ b/pkg/spec/gitops_test.go @@ -3750,6 +3750,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/pkg/spec/gitops_validate.go b/pkg/spec/gitops_validate.go index 7f57d3b7e9e..f9fdfb52b9c 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.Cut(field.Tag.Get("renameto"), ","); 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) { 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" diff --git a/server/fleet/app.go b/server/fleet/app.go index 968b4b2161a..d4e81a5f3bd 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -188,12 +188,13 @@ 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 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,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 5bc32a0548a..b38d21393c0 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -728,13 +728,13 @@ 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 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/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/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..37f04492286 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{}) + s.Require().Equal([]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..cbbeebe259e 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]struct{}, len(rules)) for _, r := range rules { oldToNew[r.OldKey] = r.NewKey newToOld[r.NewKey] = r.OldKey + if r.Inline { + inlineOld[r.OldKey] = struct{}{} + } } 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 _, 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) + 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..985378a2916 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.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.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") + + // 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.InDelta(t, float64(22), newFleet["fleet_id"], 0) + }, + }, + { + // 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 3b6eaca084a..0fee6783473 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/appconfig.go b/server/service/appconfig.go index 71de614232f..6c131ce2612 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -1987,7 +1987,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 } @@ -2020,13 +2020,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 df59183f1aa..7a50f81eb48 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -6952,7 +6952,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,inline"` Err error `json:"error,omitempty"` } @@ -7022,7 +7022,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,inline"` } func (r listABMTokensResponse) Error() error { return r.Err } @@ -7089,7 +7089,7 @@ type updateABMTokenTeamsRequest struct { } type updateABMTokenTeamsResponse struct { - ABMToken *fleet.ABMToken `json:"abm_token,omitempty"` + ABMToken *fleet.ABMToken `json:"abm_token,omitempty" renameto:"ab_token,inline"` Err error `json:"error,omitempty"` } @@ -7152,7 +7152,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,inline"` Err error `json:"error,omitempty"` } diff --git a/server/service/client.go b/server/service/client.go index 521a948dd38..99dca54e950 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..f3d9352f553 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 @@ -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 b75d42d42cf..f4b26d7885b 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -836,13 +836,13 @@ 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.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.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) + 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..d2ec988bfe9 100644 --- a/server/service/handler_deprecated_paths.go +++ b/server/service/handler_deprecated_paths.go @@ -213,11 +213,41 @@ 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"}, + }, + { + 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 7e53c37b1a2..d46dc3ab6da 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -11979,16 +11979,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.NoError(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) @@ -12005,7 +12011,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) @@ -12014,7 +12020,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) @@ -12026,7 +12032,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)