From 20c0728ac92b5ed08f264c756d1352dc77bf8091 Mon Sep 17 00:00:00 2001
From: gillespi314 <73313222+gillespi314@users.noreply.github.com>
Date: Fri, 15 Nov 2024 13:26:14 -0600
Subject: [PATCH 1/4] Apply minimum OS version enforcement to MDM SSO endpoint
---
changes/22361-os-update-ade-sso | 2 ++
cmd/fleet/serve.go | 3 +++
server/service/handler.go | 42 +++++++++++++++++++++++++++++++++
3 files changed, 47 insertions(+)
create mode 100644 changes/22361-os-update-ade-sso
diff --git a/changes/22361-os-update-ade-sso b/changes/22361-os-update-ade-sso
new file mode 100644
index 000000000000..40221866fb93
--- /dev/null
+++ b/changes/22361-os-update-ade-sso
@@ -0,0 +1,2 @@
+- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM
+ IdP integration was enabled.
diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go
index 4c836bebdccd..0504891acc7a 100644
--- a/cmd/fleet/serve.go
+++ b/cmd/fleet/serve.go
@@ -1027,6 +1027,9 @@ the way that the Fleet server works.
"get_frontend",
service.ServeFrontend(config.Server.URLPrefix, config.Server.SandboxEnabled, httpLogger),
)
+
+ frontendHandler = service.WithMDMEnrollmentMiddleware(svc, httpLogger, frontendHandler, config.Server.URLPrefix)
+
apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore)
setupRequired, err := svc.SetupRequired(baseCtx)
diff --git a/server/service/handler.go b/server/service/handler.go
index 4f916f576ba2..535ac29fc36f 100644
--- a/server/service/handler.go
+++ b/server/service/handler.go
@@ -2,6 +2,7 @@ package service
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"net/http"
@@ -1225,3 +1226,44 @@ func registerMDM(
mux.Handle(apple_mdm.MDMPath, mdmHandler)
return nil
}
+
+func WithMDMEnrollmentMiddleware(svc fleet.Service, logger kitlog.Logger, next http.Handler, urlPrefix string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/mdm/sso" {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ // if x-apple-aspen-deviceinfo custom header is present, we need to check for minimum os version
+ di := r.Header.Get("x-apple-aspen-deviceinfo")
+ if di != "" {
+ parsed, err := apple_mdm.ParseDeviceinfo(di, false) // FIXME: use verify=true when we have better parsing for various Apple certs (https://github.com/fleetdm/fleet/issues/20879)
+ if err != nil {
+ // just log the error and continue to next
+ level.Error(logger).Log("msg", "parsing x-apple-aspen-deviceinfo", "err", err)
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(context.Background(), parsed)
+ if err != nil {
+ // just log the error and continue to next
+ level.Error(logger).Log("msg", "checking minimum os version for mdm", "err", err)
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ if sur != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ if err := json.NewEncoder(w).Encode(sur); err != nil {
+ level.Error(logger).Log("msg", "failed to encode software update required", "err", err)
+ http.Redirect(w, r, r.URL.String()+"?error=true", http.StatusSeeOther)
+ }
+ return
+ }
+ }
+
+ next.ServeHTTP(w, r)
+ }
+}
From d1363ff28c4e86eb6e36b107e64f06ae55da0f7e Mon Sep 17 00:00:00 2001
From: gillespi314 <73313222+gillespi314@users.noreply.github.com>
Date: Tue, 19 Nov 2024 15:46:26 -0600
Subject: [PATCH 2/4] Update tests
---
server/service/apple_mdm.go | 8 +-
server/service/apple_mdm_test.go | 2 +-
server/service/integration_mdm_dep_test.go | 495 ++++++++++++++++++++-
server/service/integration_mdm_test.go | 1 +
server/service/testing_utils.go | 6 +
5 files changed, 492 insertions(+), 20 deletions(-)
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index aa6fcaf05a7a..7c29fc86a36b 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -1526,7 +1526,13 @@ func (svc *Service) needsOSUpdateForDEPEnrollment(ctx context.Context, m fleet.M
return false, nil
}
- return apple_mdm.IsLessThanVersion(m.OSVersion, settings.MinimumVersion.Value)
+ needsUpdate, err := apple_mdm.IsLessThanVersion(m.OSVersion, settings.MinimumVersion.Value)
+ if err != nil {
+ level.Info(svc.logger).Log("msg", "checking os updates settings, cannot compare versions", "serial", m.Serial, "current_version", m.OSVersion, "minimum_version", settings.MinimumVersion.Value)
+ return false, nil
+ }
+
+ return needsUpdate, nil
}
func (svc *Service) getAppleSoftwareUpdateRequiredForDEPEnrollment(m fleet.MDMAppleMachineInfo) (*fleet.MDMAppleSoftwareUpdateRequired, error) {
diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go
index 60ae723f9a7a..be4f75680bf2 100644
--- a/server/service/apple_mdm_test.go
+++ b/server/service/apple_mdm_test.go
@@ -4093,7 +4093,7 @@ func TestCheckMDMAppleEnrollmentWithMinimumOSVersion(t *testing.T) {
SoftwareUpdateDeviceID: "J516sAP",
},
updateRequired: nil,
- err: "invalid current version",
+ err: "", // no error, allow enrollment to proceed without software update
},
}
diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go
index 50ec5112cc17..b8350fcd5ef9 100644
--- a/server/service/integration_mdm_dep_test.go
+++ b/server/service/integration_mdm_dep_test.go
@@ -1,7 +1,13 @@
package service
import (
+ "bytes"
"context"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -10,18 +16,22 @@ import (
"os"
"path/filepath"
"slices"
+ "strconv"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
+ "github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
+ apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
+ "github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/worker"
kitlog "github.com/go-kit/log"
@@ -31,6 +41,7 @@ import (
micromdm "github.com/micromdm/micromdm/mdm/mdm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "go.mozilla.org/pkcs7"
)
type profileAssignmentReq struct {
@@ -1046,14 +1057,22 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
deletedSerial = devices[1].SerialNumber
devices = []godep.Device{
{SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now()},
- {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "modified",
- OpDate: time.Now().Add(time.Second)},
- {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted",
- OpDate: time.Now().Add(2 * time.Second)},
- {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added",
- OpDate: time.Now().Add(3 * time.Second)},
- {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted",
- OpDate: time.Now().Add(4 * time.Second)},
+ {
+ SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "modified",
+ OpDate: time.Now().Add(time.Second),
+ },
+ {
+ SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(2 * time.Second),
+ },
+ {
+ SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added",
+ OpDate: time.Now().Add(3 * time.Second),
+ },
+ {
+ SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(4 * time.Second),
+ },
{SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", OpDate: time.Now()},
{SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now().Add(time.Second)},
@@ -1436,7 +1455,8 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() {
RetryJobID uint `db:"retry_job_id"`
}
checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string,
- expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow {
+ expectedStatus fleet.DEPAssignProfileResponseStatus,
+ ) map[string]hostDEPRow {
bySerial := make(map[string]hostDEPRow, len(deviceSerials))
for _, deviceSerial := range deviceSerials {
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
@@ -1627,14 +1647,18 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() {
devices = []godep.Device{
{SerialNumber: devices[0].SerialNumber, Model: "MacBook Pro M1", OS: "osx", OpType: "added", OpDate: time.Now()},
{SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted", OpDate: time.Now()},
- {SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "added",
- OpDate: time.Now().Add(time.Microsecond)},
+ {
+ SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "added",
+ OpDate: time.Now().Add(time.Microsecond),
+ },
}
defaultOrgDevices = []godep.Device{
{SerialNumber: defaultOrgDevices[0].SerialNumber, Model: "MacBook Mini M2", OS: "osx", OpType: "added", OpDate: time.Now()},
{SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted", OpDate: time.Now()},
- {SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "added",
- OpDate: time.Now().Add(time.Microsecond)},
+ {
+ SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "added",
+ OpDate: time.Now().Add(time.Microsecond),
+ },
}
// trigger a profile sync
@@ -1663,13 +1687,17 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() {
// Delete the devices
devices = []godep.Device{
{SerialNumber: devices[0].SerialNumber, Model: "MacBook Pro M1", OS: "osx", OpType: "modified", OpDate: time.Now()},
- {SerialNumber: devices[2].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted",
- OpDate: time.Now().Add(time.Microsecond)},
+ {
+ SerialNumber: devices[2].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(time.Microsecond),
+ },
}
defaultOrgDevices = []godep.Device{
{SerialNumber: defaultOrgDevices[0].SerialNumber, Model: "MacBook Mini M2", OS: "osx", OpType: "modified", OpDate: time.Now()},
- {SerialNumber: defaultOrgDevices[2].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted",
- OpDate: time.Now().Add(time.Microsecond)},
+ {
+ SerialNumber: defaultOrgDevices[2].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(time.Microsecond),
+ },
}
// trigger a profile sync
@@ -1694,7 +1722,6 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() {
checkHostDEPAssignProfileResponses(defaultSerials, defaultProfileUUIDs[len(defaultProfileUUIDs)-1],
fleet.DEPAssignProfileResponseSuccess)
checkHostDEPAssignProfileResponses(teamSerials, teamProfileUUIDs[len(teamProfileUUIDs)-1], fleet.DEPAssignProfileResponseSuccess)
-
}
func (s *integrationMDMTestSuite) TestDeprecatedDefaultAppleBMTeam() {
@@ -2342,3 +2369,435 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptFo
require.Equal(t, 1, deviceConfiguredCount)
require.Equal(t, 0, otherCount)
}
+
+func (s *integrationMDMTestSuite) TestEnforceMiniumOSVersion() {
+ t := s.T()
+ s.enableABM(t.Name())
+
+ devices := []godep.Device{
+ {SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"},
+ {SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"},
+ }
+ profileAssignmentReqs := []profileAssignmentReq{}
+
+ s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ encoder := json.NewEncoder(w)
+ switch r.URL.Path {
+ case "/session":
+ err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
+ require.NoError(t, err)
+ case "/profile":
+ err := encoder.Encode(godep.ProfileResponse{ProfileUUID: uuid.New().String()})
+ require.NoError(t, err)
+ case "/server/devices":
+ // This endpoint is used to get an initial list of
+ // devices, return a single device
+ err := encoder.Encode(godep.DeviceResponse{Devices: devices})
+ require.NoError(t, err)
+ case "/devices/sync":
+ // This endpoint is polled over time to sync devices from
+ // ABM, send a repeated serial and a new one
+ err := encoder.Encode(godep.DeviceResponse{Devices: devices, Cursor: "foo"})
+ require.NoError(t, err)
+ case "/profile/devices":
+ b, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var prof profileAssignmentReq
+ require.NoError(t, json.Unmarshal(b, &prof))
+ profileAssignmentReqs = append(profileAssignmentReqs, prof)
+ var resp godep.ProfileResponse
+ resp.ProfileUUID = prof.ProfileUUID
+ resp.Devices = make(map[string]string, len(prof.Devices))
+ for _, device := range prof.Devices {
+ resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess)
+ }
+ err = encoder.Encode(resp)
+ require.NoError(t, err)
+ default:
+ _, _ = w.Write([]byte(`{}`))
+ }
+ }))
+
+ s.runDEPSchedule()
+
+ listHostsRes := listHostsResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
+ require.Len(t, listHostsRes.Hosts, 2)
+
+ token := loadEnrollmentProfileDEPToken(t, s.ds)
+
+ encodeDeviceInfo := func(machineInfo fleet.MDMAppleMachineInfo) string {
+ body, err := plist.Marshal(machineInfo)
+ require.NoError(t, err)
+
+ // body is expected to be a PKCS7 signed message, although we don't currently verify the signature
+ signedData, err := pkcs7.NewSignedData(body)
+ require.NoError(t, err)
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ require.NoError(t, err)
+ crtBytes, err := depot.NewCACert().SelfSign(rand.Reader, key.Public(), key)
+ require.NoError(t, err)
+ crt, err := x509.ParseCertificate(crtBytes)
+ require.NoError(t, err)
+ require.NoError(t, signedData.AddSigner(crt, key, pkcs7.SignerInfoConfig{}))
+ sig, err := signedData.Finish()
+ require.NoError(t, err)
+
+ return base64.StdEncoding.EncodeToString(sig)
+ }
+
+ fetchEnrollProfile := func(machineInfo *fleet.MDMAppleMachineInfo, expectEnrollInfo *mdmtest.AppleEnrollInfo, expectSoftwareUpdate *fleet.MDMAppleSoftwareUpdateRequiredDetails) error {
+ request, err := http.NewRequest("GET", s.server.URL+apple_mdm.EnrollPath+"?token="+token, nil)
+ if err != nil {
+ return fmt.Errorf("create request: %w", err)
+ }
+ if machineInfo != nil {
+ request.Header.Set("x-apple-aspen-deviceinfo", encodeDeviceInfo(*machineInfo))
+ }
+
+ // #nosec (this client is used for testing only)
+ cc := fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{
+ InsecureSkipVerify: true,
+ }))
+ response, err := cc.Do(request)
+ if err != nil {
+ return fmt.Errorf("send request: %w", err)
+ }
+ defer response.Body.Close()
+
+ switch {
+ case expectEnrollInfo != nil:
+ require.Equal(t, http.StatusOK, response.StatusCode)
+ body, err := io.ReadAll(response.Body)
+ if err != nil {
+ return fmt.Errorf("read body: %w", err)
+ }
+ rawProfile := body
+ if !bytes.HasPrefix(rawProfile, []byte(" 0 && opts[0].WithDEPWebview {
+ frontendHandler := WithMDMEnrollmentMiddleware(svc, logger, ServeFrontend("", false, logger), "")
+ rootMux.Handle("/", frontendHandler)
+ }
+
apiHandler := MakeHandler(svc, cfg, logger, limitStore, WithLoginRateLimit(throttled.PerMin(1000)))
rootMux.Handle("/api/", apiHandler)
var errHandler *errorstore.Handler
From 19bc9c954f787c71eb77e59d3322028515803bf3 Mon Sep 17 00:00:00 2001
From: gillespi314 <73313222+gillespi314@users.noreply.github.com>
Date: Tue, 19 Nov 2024 17:25:09 -0600
Subject: [PATCH 3/4] Update tests
---
server/service/integration_mdm_dep_test.go | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go
index 09340fa81ac9..8f36fcada5d2 100644
--- a/server/service/integration_mdm_dep_test.go
+++ b/server/service/integration_mdm_dep_test.go
@@ -16,7 +16,6 @@ import (
"os"
"path/filepath"
"slices"
- "strconv"
"strings"
"testing"
"time"
@@ -2769,7 +2768,7 @@ func (s *integrationMDMTestSuite) TestEnforceMiniumOSVersion() {
} else {
tcResp := teamResponse{}
- s.DoJSON("PATCH", "/api/latest/fleet/teams/"+strconv.Itoa(int(*teamId)), json.RawMessage(fmt.Sprintf(`{ "mdm": { "macos_updates": { "minimum_version": "%s", "deadline": "2023-12-31" } } }`, minVersion)), http.StatusOK, &tcResp)
+ s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", *teamId), json.RawMessage(fmt.Sprintf(`{ "mdm": { "macos_updates": { "minimum_version": "%s", "deadline": "2023-12-31" } } }`, minVersion)), http.StatusOK, &tcResp)
}
}
@@ -2913,6 +2912,11 @@ func (s *integrationMDMTestSuite) TestEnforceMiniumOSVersion() {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
+ if tc.updateRequired == nil {
+ // skip these test cases that depend more heavily on the frontent
+ // integration, we're just testing the error handling here
+ return
+ }
var mi fleet.MDMAppleMachineInfo
if tc.machineInfo != nil {
mi = *tc.machineInfo
@@ -2980,7 +2984,7 @@ func (s *integrationMDMTestSuite) TestEnforceMiniumOSVersion() {
t.Run("sso enabled", func(t *testing.T) {
tcResp := teamResponse{}
- s.DoJSON("PATCH", "/api/latest/fleet/teams/"+strconv.Itoa(int(team.ID)), json.RawMessage(fmt.Sprintf(`{ "mdm": { "macos_setup": { "enable_end_user_authentication": true } } }`)), http.StatusOK, &tcResp)
+ s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), json.RawMessage(fmt.Sprintf(`{ "mdm": { "macos_setup": { "enable_end_user_authentication": true } } }`)), http.StatusOK, &tcResp)
acResp := appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
@@ -2989,6 +2993,11 @@ func (s *integrationMDMTestSuite) TestEnforceMiniumOSVersion() {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
+ if tc.updateRequired == nil {
+ // skip these test cases that depend more heavily on the frontent
+ // integration, we're just testing the error handling here
+ return
+ }
if tc.machineInfo != nil {
tc.machineInfo.Serial = devices[0].SerialNumber
}
From 9ffe86edbe349e9ab52248646931bb17f9e87df6 Mon Sep 17 00:00:00 2001
From: gillespi314 <73313222+gillespi314@users.noreply.github.com>
Date: Tue, 19 Nov 2024 17:41:25 -0600
Subject: [PATCH 4/4] Fix lint
---
server/service/integration_mdm_dep_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go
index 8f36fcada5d2..53046106af53 100644
--- a/server/service/integration_mdm_dep_test.go
+++ b/server/service/integration_mdm_dep_test.go
@@ -2984,7 +2984,7 @@ func (s *integrationMDMTestSuite) TestEnforceMiniumOSVersion() {
t.Run("sso enabled", func(t *testing.T) {
tcResp := teamResponse{}
- s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), json.RawMessage(fmt.Sprintf(`{ "mdm": { "macos_setup": { "enable_end_user_authentication": true } } }`)), http.StatusOK, &tcResp)
+ s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), json.RawMessage(`{ "mdm": { "macos_setup": { "enable_end_user_authentication": true } } }`), http.StatusOK, &tcResp)
acResp := appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{