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:
@@ -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