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