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 4923cb95ad59..4a830c2de339 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -1029,6 +1029,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/apple_mdm.go b/server/service/apple_mdm.go index 0e0255a09799..da6d28be49bc 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/handler.go b/server/service/handler.go index 147acacf4402..bfa898fd8f46 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" @@ -1231,3 +1232,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) + } +} diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index c03e36c4a531..626f0d191f65 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" @@ -15,13 +21,16 @@ import ( "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 +40,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 { @@ -2652,3 +2662,445 @@ func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingItFromABM // make sure the host gets post enrollment requests checkPostEnrollmentCommands(mdmDevice, true) } + +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