42508 Rename abm to ab in API (#46657)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #42508 

Renames abm/apple_business_manager to ab/apple_business in API and
fleetctl. Uses existing renameto logic with a slight twist: added
"inline" option to handle cases particularly where a single object tree
has renames in multiple versions so that we don't break backwards
compatibiility since the default behavior when you have multi-level
renames is a new/old split at the top level

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Canonical Apple Business (AB) API endpoints and CLI:
/api/v1/fleet/ab_tokens, /api/v1/fleet/mdm/apple/ab_public_key, plus new
fleetctl get mdm-ab and fleetctl generate mdm-ab
  * New GitOps/config key: mdm.apple_business
* Admin UI updated to show Apple Business tokens with fleet-based
associations and updated labels

* **Deprecations**
* Legacy ABM endpoints, CLI aliases, and config keys remain supported
but emit deprecation warnings pointing to the new AB equivalents
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jordan Montgomery
2026-06-03 14:58:17 -04:00
committed by GitHub
parent 2b822ac0ee
commit 356caea6fd
51 changed files with 612 additions and 245 deletions
+1
View File
@@ -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.
+64 -41
View File
@@ -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
}
+5 -2
View File
@@ -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")
@@ -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",
+66 -44
View File
@@ -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,60 +1635,80 @@ 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")
}
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)
}
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 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")
}
return nil
return runGetMDMAB(c)
},
}
}
func getMDMABFlags() []cli.Flag {
return []cli.Flag{
configFlag(),
contextFlag(),
debugFlag(),
}
}
func runGetMDMAB(c *cli.Context) error {
const expirationWarning = 30 * 24 * time.Hour // 30 days
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 {
return &cli.Command{
Name: "mdm-command-results",
+4 -4
View File
@@ -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")
})
}
+9 -6
View File
@@ -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)
@@ -1000,7 +1000,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)
@@ -1125,13 +1128,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,
},
}
}
+2 -2
View File
@@ -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")
},
},
{
@@ -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
@@ -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,
@@ -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,
@@ -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
@@ -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
@@ -66,6 +66,7 @@
},
"mdm": {
"android_enabled_and_configured": false,
"apple_business": null,
"apple_business_manager": null,
"apple_server_url": "",
"volume_purchasing_program": null,
@@ -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
@@ -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"
@@ -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"
@@ -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"
@@ -33,6 +33,7 @@ spec:
zendesk: null
mdm:
android_enabled_and_configured: false
apple_business:
apple_business_manager:
apple_server_url: ""
volume_purchasing_program:
@@ -33,6 +33,7 @@ spec:
zendesk: null
mdm:
android_enabled_and_configured: false
apple_business:
apple_business_manager:
apple_server_url: ""
volume_purchasing_program:
+1 -1
View File
@@ -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]
+8 -8
View File
@@ -19,7 +19,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,
@@ -116,18 +116,18 @@ const App = ({ children, location }: IAppProps): JSX.Element => {
},
});
// Get the ABM tokens
useQuery<IGetAbmTokensResponse, AxiosError>(
["abm_tokens"],
// Get the Apple Business (AB) tokens
useQuery<IGetAbTokensResponse, AxiosError>(
["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
),
});
+9 -4
View File
@@ -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 {
+6 -1
View File
@@ -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;
@@ -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<IMdmAbmToken | null>(null);
const selectedToken = useRef<IMdmAbToken | null>(null);
const {
data: abmTokens,
data: abTokens,
error: errorAbmTokens,
isLoading,
isRefetching,
refetch,
} = useQuery<IGetAbmTokensResponse, AxiosError, IMdmAbmToken[]>(
["abmTokens"],
} = useQuery<IGetAbTokensResponse, AxiosError, IMdmAbToken[]>(
["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 <DataError verticalPaddingSize="pad-xxxlarge" />;
}
if (abmTokens?.length === 0) {
if (abTokens?.length === 0) {
return <AddAbmMessage onAddAbm={onAddAbm} />;
}
if (abmTokens) {
if (abTokens) {
return (
<>
<p>
@@ -188,7 +188,7 @@ const AppleBusinessManagerPage = ({ router }: { router: InjectedRouter }) => {
hosts.
</p>
<AppleBusinessManagerTable
abmTokens={abmTokens}
abTokens={abTokens}
onEditTokenTeam={onEditTokenTeam}
onRenewToken={onRenewToken}
onDeleteToken={onDeleteToken}
@@ -214,7 +214,7 @@ const AppleBusinessManagerPage = ({ router }: { router: InjectedRouter }) => {
<div className={`${baseClass}__page-header-section`}>
<h1>Apple Business (AB)</h1>
{isPremiumTier &&
abmTokens?.length !== 0 &&
abTokens?.length !== 0 &&
!!config?.mdm.enabled_and_configured && (
<Button onClick={onAddAbm}>Add AB</Button>
)}
@@ -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 (
<TableContainer<IMdmAbmToken>
<TableContainer<IMdmAbToken>
columnConfigs={tableConfig}
defaultSortHeader="org_name"
disableTableHeader
@@ -56,7 +56,7 @@ const AppleBusinessManagerTable = ({
isAllPagesSelected={false}
emptyComponent={() => <></>}
isLoading={false}
data={abmTokens}
data={abTokens}
className={baseClass}
/>
);
@@ -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<IMdmAbmToken>;
type ITableStringCellProps = IStringCellProps<IMdmAbmToken>;
type IRenewDateCellProps = CellProps<IMdmAbmToken, IMdmAbmToken["renew_date"]>;
type IAbmTableConfig = Column<IMdmAbToken>;
type ITableStringCellProps = IStringCellProps<IMdmAbToken>;
type IRenewDateCellProps = CellProps<IMdmAbToken, IMdmAbToken["renew_date"]>;
type ITableHeaderProps = IHeaderProps<IMdmAbmToken>;
type ITableHeaderProps = IHeaderProps<IMdmAbToken>;
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 = (
<TooltipWrapper
@@ -137,7 +137,7 @@ export const generateTableConfig = (
},
{
id: "ios_team",
accessor: (originalRow) => getTeamDisplayName(originalRow.ios_team),
accessor: (originalRow) => getFleetDisplayName(originalRow.ios_fleet),
Header: () => {
const titleWithToolTip = (
<TooltipWrapper
@@ -162,7 +162,7 @@ export const generateTableConfig = (
},
{
id: "ipados_team",
accessor: (originalRow) => getTeamDisplayName(originalRow.ipados_team),
accessor: (originalRow) => getFleetDisplayName(originalRow.ipados_fleet),
Header: () => {
const titleWithToolTip = (
<TooltipWrapper
@@ -208,6 +208,6 @@ export const generateTableConfig = (
];
};
export const generateTableData = (data: IMdmAbmToken[]) => {
export const generateTableData = (data: IMdmAbToken[]) => {
return data;
};
@@ -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<SelectedTeamNames>(
{
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,
}
);
@@ -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;
};
+21 -21
View File
@@ -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<IMdmAbmToken> => {
const { MDM_ABM_TOKENS } = endpoints;
uploadToken: (token: File): Promise<IMdmAbToken> => {
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<IAbmTokenResponse> => {
const { MDM_ABM_TOKEN_RENEW } = endpoints;
const path = MDM_ABM_TOKEN_RENEW(id);
renewToken: (id: number, token: File): Promise<IAbTokenResponse> => {
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<void> => {
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<IGetAbmTokensResponse> => {
const { MDM_ABM_TOKENS } = endpoints;
return sendRequest("GET", MDM_ABM_TOKENS);
getTokens: (): Promise<IGetAbTokensResponse> => {
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);
},
};
+7 -7
View File
@@ -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?
+10 -5
View File
@@ -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"},
+73
View File
@@ -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.
+3 -2
View File
@@ -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,
+7
View File
@@ -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) {
+10 -2
View File
@@ -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"
+5 -4
View File
@@ -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
+4 -4
View File
@@ -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"`
+1 -1
View File
@@ -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."
+7 -2
View File
@@ -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)
@@ -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"`
@@ -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
}
@@ -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}]`,
@@ -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)
+3 -3
View File
@@ -1964,7 +1964,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
}
@@ -1997,13 +1997,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
}
+4 -4
View File
@@ -6941,7 +6941,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"`
}
@@ -7011,7 +7011,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 }
@@ -7078,7 +7078,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"`
}
@@ -7141,7 +7141,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"`
}
+2 -2
View File
@@ -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{}
}
}
+2 -2
View File
@@ -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
+8 -8
View File
@@ -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
+32 -2
View File
@@ -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"},
},
}
+11 -5
View File
@@ -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)