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:
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+14
-14
@@ -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>
|
||||
)}
|
||||
|
||||
+9
-9
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
+11
-11
@@ -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;
|
||||
};
|
||||
|
||||
+8
-8
@@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user