diff --git a/changes/issue-12261-microsoft-mdm-discovery-endpoint b/changes/issue-12261-microsoft-mdm-discovery-endpoint
new file mode 100644
index 000000000..805a2e490
--- /dev/null
+++ b/changes/issue-12261-microsoft-mdm-discovery-endpoint
@@ -0,0 +1 @@
+* Microsoft MDM Enrollment Protocol: Added support for the DiscoveryRequest messages
diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go
index 8d014a661..fde38e4d8 100644
--- a/cmd/fleetctl/apply_test.go
+++ b/cmd/fleetctl/apply_test.go
@@ -898,7 +898,7 @@ spec:
emptySetupAsst := writeTmpJSON(t, map[string]any{})
// Apply global config with custom setting and macos setup assistant, and enable
- // Windows MDM.
+ // Microsoft MDM.
name = writeTmpYml(t, fmt.Sprintf(`---
apiVersion: v1
kind: config
@@ -929,7 +929,7 @@ spec:
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath},
},
- WindowsEnabledAndConfigured: true,
+ MicrosoftEnabledAndConfigured: true,
}, currentAppConfig.MDM)
// start a server to return the bootstrap package
@@ -962,7 +962,7 @@ spec:
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath},
},
- WindowsEnabledAndConfigured: true,
+ MicrosoftEnabledAndConfigured: true,
}, currentAppConfig.MDM)
// Apply team config.
diff --git a/docs/Using-Fleet/configuration-files/README.md b/docs/Using-Fleet/configuration-files/README.md
index 4fe3cbf07..26e89ccbb 100644
--- a/docs/Using-Fleet/configuration-files/README.md
+++ b/docs/Using-Fleet/configuration-files/README.md
@@ -1379,7 +1379,7 @@ Set name of default team to use with Apple Business Manager.
##### mdm.windows_enabled_and_configured
-Enables or disables Windows MDM support.
+Enables or disables Microsoft MDM support.
- Default value: false
- Config file format:
diff --git a/orbit/pkg/table/mdm/mdm_windows.go b/orbit/pkg/table/mdm/mdm_windows.go
index 7732b23ca..9c66e76eb 100644
--- a/orbit/pkg/table/mdm/mdm_windows.go
+++ b/orbit/pkg/table/mdm/mdm_windows.go
@@ -398,7 +398,7 @@ func executeMDMcommand(inputCMD string) (string, error) {
return "", fmt.Errorf("there was an error calling ApplyLocalManagementSyncML(): (0x%X)", err, returnCode)
}
- // converting windows MDM UTF16 output string into go string
+ // converting Microsoft MDM UTF16 output string into go string
outputCmd, err := localUTF16toString(unsafe.Pointer(outputStrBuffer))
if err != nil {
return "", err
diff --git a/server/config/config.go b/server/config/config.go
index 0059e4396..f63c7db67 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -1659,11 +1659,11 @@ func SetTestMDMConfig(t testing.TB, cfg *FleetConfig, cert, key []byte, appleBMT
cfg.MDM.AppleSCEPChallenge = "testchallenge"
}
-// Undocumented feature flag for Windows MDM, used to determine if the Windows
+// Undocumented feature flag for Microsoft MDM, used to determine if the Windows
// MDM feature is visible in the UI and can be enabled. More details here:
// https://github.com/fleetdm/fleet/issues/12257
//
-// TODO: remove this flag once the Windows MDM feature is ready for
+// TODO: remove this flag once the Microsoft MDM feature is ready for
// release.
func IsMDMFeatureFlagEnabled() bool {
return os.Getenv("FLEET_DEV_MDM_ENABLED") == "1"
diff --git a/server/fleet/app.go b/server/fleet/app.go
index cc63f1251..0882b037f 100644
--- a/server/fleet/app.go
+++ b/server/fleet/app.go
@@ -151,11 +151,11 @@ type MDM struct {
MacOSMigration MacOSMigration `json:"macos_migration"`
EndUserAuthentication MDMEndUserAuthentication `json:"end_user_authentication"`
- // WindowsEnabledAndConfigured indicates if Fleet MDM is enabled for Windows.
+ // MicrosoftEnabledAndConfigured indicates if Fleet MDM is enabled for Windows.
// There is no other configuration required for Windows other than enabling
// the support, but it is still called "EnabledAndConfigured" for consistency
// with the similarly named macOS-specific fields.
- WindowsEnabledAndConfigured bool `json:"windows_enabled_and_configured"`
+ MicrosoftEnabledAndConfigured bool `json:"windows_enabled_and_configured"`
/////////////////////////////////////////////////////////////////
// WARNING: If you add to this struct make sure it's taken into
diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go
index 3cc63a271..68d2a0907 100644
--- a/server/fleet/hosts.go
+++ b/server/fleet/hosts.go
@@ -555,9 +555,9 @@ func (h *Host) NeedsDEPEnrollment() bool {
h.IsDEPAssignedToFleet()
}
-// IsElegibleForWindowsMDMEnrollment returns true if the host can be enrolled
-// in Fleet's Windows MDM (if Windows MDM was enabled).
-func (h *Host) IsElegibleForWindowsMDMEnrollment() bool {
+// IsElegibleForMicrosoftMDMEnrollment returns true if the host can be enrolled
+// in Fleet's Microsoft MDM (if Microsoft MDM was enabled).
+func (h *Host) IsElegibleForMicrosoftMDMEnrollment() bool {
return h.FleetPlatform() == "windows" &&
h.IsOsqueryEnrolled() &&
!h.MDMInfo.IsEnrolledInThirdPartyMDM() &&
diff --git a/server/fleet/microsoft_mdm.go b/server/fleet/microsoft_mdm.go
new file mode 100644
index 000000000..d878ac8ec
--- /dev/null
+++ b/server/fleet/microsoft_mdm.go
@@ -0,0 +1,343 @@
+package fleet
+
+import (
+ "encoding/xml"
+)
+
+// MS-MDE2 Message request types
+const (
+ MDEDiscovery = iota
+ MDEPolicy
+ MDEEnrollment
+ MDEFault
+)
+
+///////////////////////////////////////////////////////////////
+/// Microsoft MS-MDE2 SOAP types
+
+// ResponseHeader is the header for MDM responses from the server
+type ResponseHeader struct {
+ Action Action `xml:"Action"`
+ RelatesTo string `xml:"a:RelatesTo"`
+ ActivityId *ActivityId `xml:"ActivityId,omitempty"`
+ Security *WsSecurity `xml:"o:Security,omitempty"`
+}
+
+// RequestHeader is the header for MDM requests to the server
+type RequestHeader struct {
+ Action Action `xml:"Action"`
+ MessageID string `xml:"MessageID"`
+ ReplyTo ReplyTo `xml:"ReplyTo"`
+ To To `xml:"To"`
+ Security *TokenSecurity `xml:"Security,omitempty"`
+}
+
+// BodyReponse is the body of the MDM SOAP response message
+type BodyResponse struct {
+ Xsd *string `xml:"xmlns:xsd,attr,omitempty"`
+ Xsi *string `xml:"xmlns:xsi,attr,omitempty"`
+ DiscoverResponse *DiscoverResponse `xml:"DiscoverResponse,omitempty"`
+ GetPoliciesResponse *GetPoliciesResponse `xml:"GetPoliciesResponse,omitempty"`
+ RequestSecurityTokenResponseCollection *RequestSecurityTokenResponseCollection `xml:"RequestSecurityTokenResponseCollection,omitempty"`
+ SoapFault *SoapFault `xml:"s:fault,omitempty"`
+}
+
+// BodyRequest is the body of the MDM SOAP request message
+type BodyRequest struct {
+ Xsi *string `xml:"xsi,attr,omitempty"`
+ Xsd *string `xml:"xsd,attr,omitempty"`
+ Discover *Discover `xml:"Discover,omitempty"`
+ GetPolicies *GetPolicies `xml:"GetPolicies,omitempty"`
+ RequestSecurityToken *RequestSecurityToken `xml:"RequestSecurityToken,omitempty"`
+}
+
+// HTTP request header field used to indicate the intent of the SOAP request, using a URI value
+// See section 6.1.1 on SOAP Spec - https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383527
+type Action struct {
+ Content string `xml:",chardata"`
+ MustUnderstand string `xml:"mustUnderstand,attr"`
+}
+
+// ActivityId is a unique identifier for the activity
+type ActivityId struct {
+ Content string `xml:",chardata"`
+ CorrelationId string `xml:"CorrelationId,attr"`
+ XmlNS string `xml:"xmlns,attr"`
+}
+
+// Timestamp for certificate authentication
+type Timestamp struct {
+ ID string `xml:"u:Id,attr"`
+ Created string `xml:"u:Created"`
+ Expires string `xml:"u:Expires"`
+}
+
+// Security token container
+type WsSecurity struct {
+ XmlNS string `xml:"xmlns:o,attr"`
+ MustUnderstand string `xml:"s:mustUnderstand,attr"`
+ Timestamp Timestamp `xml:"u:Timestamp"`
+}
+
+// Security token container for encoded security sensitive data
+type BinSecurityToken struct {
+ Content string `xml:",chardata"`
+ Value string `xml:"ValueType,attr"`
+ Encoding string `xml:"EncodingType,attr"`
+}
+
+// TokenSecurity is the security token container for BinSecurityToken
+type TokenSecurity struct {
+ MustUnderstand string `xml:"mustUnderstand,attr"`
+ Security BinSecurityToken `xml:"BinarySecurityToken"`
+}
+
+// To target endpoint header field
+type To struct {
+ Content string `xml:",chardata"`
+ MustUnderstand string `xml:"mustUnderstand,attr"`
+}
+
+// ReplyTo message correlation header field
+type ReplyTo struct {
+ Address string `xml:"Address"`
+}
+
+///////////////////////////////////////////////////////////////
+/// Discover MS-MDE2 Message request type
+/// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/36e33def-59ab-484f-b0bc-701496346925
+
+type Discover struct {
+ XmlNS string `xml:"xmlns,attr"`
+ Request DiscoverRequest `xml:"request"`
+}
+
+type AuthPolicies struct {
+ AuthPolicy []string `xml:"AuthPolicy"`
+}
+
+type DiscoverRequest struct {
+ XmlNS string `xml:"i,attr"`
+ EmailAddress string `xml:"EmailAddress"`
+ RequestVersion string `xml:"RequestVersion"`
+ DeviceType string `xml:"DeviceType"`
+ ApplicationVersion string `xml:"ApplicationVersion"`
+ OSEdition string `xml:"OSEdition"`
+ AuthPolicies AuthPolicies `xml:"AuthPolicies"`
+}
+
+///////////////////////////////////////////////////////////////
+/// GetPolicies MS-MDE2 Message request type
+/// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/02b080e4-d1d8-4e0c-af14-b77931cec404
+
+type GetPolicies struct {
+ XmlNS string `xml:"xmlns,attr"`
+ Client Client `xml:"client"`
+ RequestFilter RequestFilter `xml:"requestFilter"`
+}
+
+type ClientContent struct {
+ Content string `xml:",chardata"`
+ Xsi string `xml:"nil,attr"`
+}
+
+type Client struct {
+ LastUpdate ClientContent `xml:"lastUpdate"`
+ PreferredLanguage ClientContent `xml:"preferredLanguage"`
+}
+
+type RequestFilter struct {
+ Xsi string `xml:"nil,attr"`
+}
+
+///////////////////////////////////////////////////////////////
+/// RequestSecurityToken MS-MDE2 Message request type
+/// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/6ba9c509-8bce-4899-85b2-8c3d41f8f845
+
+type RequestSecurityToken struct {
+ TokenType string `xml:"TokenType"`
+ RequestType string `xml:"RequestType"`
+ BinarySecurityToken BinarySecurityToken `xml:"BinarySecurityToken"`
+ AdditionalContext AdditionalContext `xml:"AdditionalContext"`
+}
+
+type BinarySecurityToken struct {
+ Content string `xml:",chardata"`
+ XmlNS *string `xml:"xmlns,attr"`
+ ValueType string `xml:"ValueType,attr"`
+ EncodingType string `xml:"EncodingType,attr"`
+}
+
+type ContextItem struct {
+ Name string `xml:"Name,attr"`
+ Value string `xml:"Value"`
+}
+
+type AdditionalContext struct {
+ XmlNS string `xml:"xmlns,attr"`
+ ContextItem []ContextItem `xml:"ContextItem"`
+}
+
+///////////////////////////////////////////////////////////////
+/// DiscoverResponse MS-MDE2 Message response type
+/// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/aa198049-e691-41f9-a45a-b973b9089be7
+
+type DiscoverResponse struct {
+ XMLName xml.Name `xml:"DiscoverResponse"`
+ XmlNS string `xml:"xmlns,attr"`
+ DiscoverResult DiscoverResult `xml:"DiscoverResult"`
+}
+
+type DiscoverResult struct {
+ AuthPolicy string `xml:"AuthPolicy"`
+ EnrollmentVersion string `xml:"EnrollmentVersion"`
+ EnrollmentPolicyServiceUrl string `xml:"EnrollmentPolicyServiceUrl"`
+ EnrollmentServiceUrl string `xml:"EnrollmentServiceUrl"`
+}
+
+///////////////////////////////////////////////////////////////
+/// GetPoliciesResponse MS-MDE2 Message response type
+/// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/6e74dcdb-c3d9-4044-af10-536224904e72
+
+type GetPoliciesResponse struct {
+ XMLName xml.Name `xml:"GetPoliciesResponse"`
+ XmlNS string `xml:"xmlns,attr"`
+ Response Response `xml:"response"`
+ OIDs OIDs `xml:"oIDs"`
+}
+
+type ContentAttr struct {
+ Content string `xml:",chardata"`
+ Xsi string `xml:"xsi:nil,attr"`
+ XmlNS string `xml:"xmlns:xsi,attr"`
+}
+
+type GenericAttr struct {
+ Xsi string `xml:"xsi:nil,attr"`
+}
+
+type CertificateValidity struct {
+ ValidityPeriodSeconds string `xml:"validityPeriodSeconds"`
+ RenewalPeriodSeconds string `xml:"renewalPeriodSeconds"`
+}
+
+type Permission struct {
+ Enroll string `xml:"enroll"`
+ AutoEnroll string `xml:"autoEnroll"`
+}
+
+type PrivateKeyAttributes struct {
+ MinimalKeyLength string `xml:"minimalKeyLength"`
+ KeySpec GenericAttr `xml:"keySpec"`
+ KeyUsageProperty GenericAttr `xml:"keyUsageProperty"`
+ Permissions GenericAttr `xml:"permissions"`
+ AlgorithmOIDReference GenericAttr `xml:"algorithmOIDReference"`
+ CryptoProviders GenericAttr `xml:"cryptoProviders"`
+}
+type Revision struct {
+ MajorRevision string `xml:"majorRevision"`
+ MinorRevision string `xml:"minorRevision"`
+}
+
+type Attributes struct {
+ CommonName string `xml:"commonName"`
+ PolicySchema string `xml:"policySchema"`
+ CertificateValidity CertificateValidity `xml:"certificateValidity"`
+ Permission Permission `xml:"permission"`
+ PrivateKeyAttributes PrivateKeyAttributes `xml:"privateKeyAttributes"`
+ Revision Revision `xml:"revision"`
+ SupersededPolicies GenericAttr `xml:"supersededPolicies"`
+ PrivateKeyFlags GenericAttr `xml:"privateKeyFlags"`
+ SubjectNameFlags GenericAttr `xml:"subjectNameFlags"`
+ EnrollmentFlags GenericAttr `xml:"enrollmentFlags"`
+ GeneralFlags GenericAttr `xml:"generalFlags"`
+ HashAlgorithmOIDReference string `xml:"hashAlgorithmOIDReference"`
+ RARequirements GenericAttr `xml:"rARequirements"`
+ KeyArchivalAttributes GenericAttr `xml:"keyArchivalAttributes"`
+ Extensions GenericAttr `xml:"extensions"`
+}
+
+type GPPolicy struct {
+ PolicyOIDReference string `xml:"policyOIDReference"`
+ CAs GenericAttr `xml:"cAs"`
+ Attributes Attributes `xml:"attributes"`
+}
+
+type Policies struct {
+ Policy GPPolicy `xml:"policy"`
+}
+
+type Response struct {
+ PolicyID string `xml:"policyID"`
+ PolicyFriendlyName ContentAttr `xml:"policyFriendlyName"`
+ NextUpdateHours ContentAttr `xml:"nextUpdateHours"`
+ PoliciesNotChanged ContentAttr `xml:"policiesNotChanged"`
+ Policies Policies `xml:"policies"`
+}
+
+type OID struct {
+ Value string `xml:"value"`
+ Group string `xml:"group"`
+ OIDReferenceID string `xml:"oIDReferenceID"`
+ DefaultName string `xml:"defaultName"`
+}
+
+type OIDs struct {
+ Content string `xml:",chardata"`
+ OID OID `xml:"oID"`
+}
+
+///////////////////////////////////////////////////////////////
+/// RequestSecurityTokenResponseCollection MS-MDE2 Message response type
+/// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/3452fe7d-2441-49f7-8801-c056b58edb6a
+
+type RequestSecurityTokenResponseCollection struct {
+ XMLName xml.Name `xml:"RequestSecurityTokenResponseCollection"`
+ XmlNS string `xml:"xmlns,attr"`
+ RequestSecurityTokenResponse RequestSecurityTokenResponse `xml:"RequestSecurityTokenResponse"`
+}
+
+type SecAttr struct {
+ Content string `xml:",chardata"`
+ XmlNS string `xml:"xmlns,attr"`
+}
+
+type RequestedSecurityToken struct {
+ BinarySecurityToken BinarySecurityToken `xml:"BinarySecurityToken"`
+}
+
+type RequestSecurityTokenResponse struct {
+ TokenType string `xml:"TokenType"`
+ DispositionMessage SecAttr `xml:"DispositionMessage"`
+ RequestedSecurityToken RequestedSecurityToken `xml:"RequestedSecurityToken"`
+ RequestID SecAttr `xml:"RequestID"`
+}
+
+///////////////////////////////////////////////////////////////
+/// SoapFault MS-MDE2 Message response type
+/// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/0a78f419-5fd7-4ddb-bc76-1c0f7e11da23
+
+type Subcode struct {
+ Value string `xml:"s:value"`
+}
+
+type Code struct {
+ Value string `xml:"s:value"`
+ Subcode Subcode `xml:"s:subcode"`
+}
+
+type ReasonText struct {
+ Content string `xml:",chardata"`
+ Lang string `xml:"xml:lang,attr"`
+}
+
+type Reason struct {
+ Text ReasonText `xml:"s:text"`
+}
+
+type SoapFault struct {
+ XMLName xml.Name `xml:"s:fault"`
+ Code Code `xml:"s:code"`
+ Reason Reason `xml:"s:reason"`
+ OriginalMessageType int `xml:"-"`
+}
diff --git a/server/fleet/orbit.go b/server/fleet/orbit.go
index 5abb12d6b..bd6ec1ac5 100644
--- a/server/fleet/orbit.go
+++ b/server/fleet/orbit.go
@@ -6,11 +6,11 @@ import "encoding/json"
// fleetd (orbit) so that it can run commands or more generally react to this
// information.
type OrbitConfigNotifications struct {
- RenewEnrollmentProfile bool `json:"renew_enrollment_profile,omitempty"`
- RotateDiskEncryptionKey bool `json:"rotate_disk_encryption_key,omitempty"`
- NeedsMDMMigration bool `json:"needs_mdm_migration,omitempty"`
- NeedsProgrammaticWindowsMDMEnrollment bool `json:"needs_programmatic_windows_mdm_enrollment,omitempty"`
- WindowsMDMDiscoveryEndpoint string `json:"windows_mdm_discovery_endpoint,omitempty"`
+ RenewEnrollmentProfile bool `json:"renew_enrollment_profile,omitempty"`
+ RotateDiskEncryptionKey bool `json:"rotate_disk_encryption_key,omitempty"`
+ NeedsMDMMigration bool `json:"needs_mdm_migration,omitempty"`
+ NeedsProgrammaticMicrosoftMDMEnrollment bool `json:"needs_programmatic_microsoft_mdm_enrollment,omitempty"`
+ MicrosoftMDMDiscoveryEndpoint string `json:"microsoft_mdm_discovery_endpoint,omitempty"`
}
type OrbitConfig struct {
diff --git a/server/fleet/service.go b/server/fleet/service.go
index 0b1e6c57d..83ef484dd 100644
--- a/server/fleet/service.go
+++ b/server/fleet/service.go
@@ -696,6 +696,11 @@ type Service interface {
// error can be raised to the user.
VerifyMDMAppleConfigured(ctx context.Context) error
+ // VerifyMDMMicrosoftConfigured verifies that the server is configured for
+ // Microsoft MDM. If an error is returned, authorization is skipped so the
+ // error can be raised to the user.
+ VerifyMDMMicrosoftConfigured(ctx context.Context) error
+
MDMAppleUploadBootstrapPackage(ctx context.Context, name string, pkg io.Reader, teamID uint) error
GetMDMAppleBootstrapPackageBytes(ctx context.Context, token string) (*MDMAppleBootstrapPackage, error)
@@ -746,4 +751,13 @@ type Service interface {
ResetAutomation(ctx context.Context, teamIDs, policyIDs []uint) error
RequestEncryptionKeyRotation(ctx context.Context, hostID uint) error
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // Microsoft MDM
+
+ // GetMDMMicrosoftDiscoveryResponse returns a valid DiscoveryResponse message
+ GetMDMMicrosoftDiscoveryResponse(ctx context.Context) (*DiscoverResponse, error)
+
+ // GetAuthorizedSoapFault authorize the request so SoapFault message can be returned
+ GetAuthorizedSoapFault(ctx context.Context, eType string, origMsg int, errorMsg error) *SoapFault
}
diff --git a/server/mdm/microsoft/microsoft_mdm.go b/server/mdm/microsoft/microsoft_mdm.go
new file mode 100644
index 000000000..598da399c
--- /dev/null
+++ b/server/mdm/microsoft/microsoft_mdm.go
@@ -0,0 +1,124 @@
+package microsoft_mdm
+
+import "github.com/fleetdm/fleet/v4/server/mdm/internal/commonmdm"
+
+const (
+ // MDMPath is Fleet's HTTP path for the core Microsoft MDM service.
+ MDMPath = "/api/mdm/microsoft"
+
+ // DiscoveryPath is the HTTP endpoint path that serves the IDiscoveryService functionality.
+ // This is the endpoint that process the Discover and DiscoverResponse messages
+ // See the section 3.1 on the MS-MDE2 specification for more details:
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/2681fd76-1997-4557-8963-cf656ab8d887
+ MDE2DiscoveryPath = MDMPath + "/discovery"
+
+ // MDE2PolicyPath is the HTTP endpoint path that delivers the X.509 Certificate Enrollment Policy (MS-XCEP) functionality.
+ // This is the endpoint that process the GetPolicies and GetPoliciesResponse messages
+ // See the section 3.3 on the MS-MDE2 specification for more details on this endpoint requirements:
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/8a5efdf8-64a9-44fd-ab63-071a26c9f2dc
+ // The MS-XCEP specification is available here:
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xcep/08ec4475-32c2-457d-8c27-5a176660a210
+ MDE2PolicyPath = MDMPath + "/policy"
+
+ // MDE2EnrollPath is the HTTP endpoint path that delivers WS-Trust X.509v3 Token Enrollment (MS-WSTEP) functionality.
+ // This is the endpoint that process the RequestSecurityToken and RequestSecurityTokenResponseCollection messages
+ // See the section 3.4 on the MS-MDE2 specification for more details on this endpoint requirements:
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/5b02c625-ced2-4a01-a8e1-da0ae84f5bb7
+ // The MS-WSTEP specification is available here:
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wstep/4766a85d-0d18-4fa1-a51f-e5cb98b752ea
+ MDE2EnrollPath = MDMPath + "/enroll"
+
+ // MDE2ManagementPath is the HTTP endpoint path that delivers WS-Trust X.509v3 Token Enrollment (MS-WSTEP) functionality.
+ // This is the endpoint that process the RequestSecurityToken and RequestSecurityTokenResponseCollection messages
+ // See the section 3.4 on the MS-MDE2 specification for more details:
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/5b02c625-ced2-4a01-a8e1-da0ae84f5bb7
+ MDE2ManagementPath = MDMPath + "/management"
+
+ // These are the entry points for the Microsoft Device Enrollment (MS-MDE) and Microsoft Device Enrollment v2 (MS-MDE2) protocols.
+ // These are required to be implemented by the MDM server to support user-driven enrollments
+ MSEnrollEntryPoint = "/EnrollmentServer/Discovery.svc"
+ MSManageEntryPoint = "/ManagementServer/MDM.svc"
+)
+
+// XML Namespaces used by the Microsoft Device Enrollment v2 protocol (MS-MDE2)
+const (
+ DiscoverNS = "http://schemas.microsoft.com/windows/management/2012/01/enrollment"
+ PolicyNS = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy"
+ EnrollWSTrust = "http://docs.oasis-open.org/ws-sx/ws-trust/200512"
+ EnrollSecExt = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
+ EnrollTType = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken"
+ EnrollPDoc = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentProvisionDoc"
+ EnrollEncode = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary"
+ EnrollReq = "http://schemas.microsoft.com/windows/pki/2009/01/enrollment"
+ EnrollNSS = "http://www.w3.org/2003/05/soap-envelope"
+ EnrollNSA = "http://www.w3.org/2005/08/addressing"
+ EnrollXSI = "http://www.w3.org/2001/XMLSchema-instance"
+ EnrollXSD = "http://www.w3.org/2001/XMLSchema"
+ EnrollXSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
+ ActionNsDiag = "http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics"
+ ActionNsDiscovery = "http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/DiscoverResponse"
+ ActionNsPolicy = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPoliciesResponse"
+ ActionNsEnroll = "http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RSTRC/wstep"
+)
+
+// Soap Error constants
+// Details here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/0a78f419-5fd7-4ddb-bc76-1c0f7e11da23
+
+const (
+ // Message format is bad
+ SoapErrorMessageFormat = "s:messageformat"
+
+ // User not recognized
+ SoapErrorAuthentication = "s:authentication"
+
+ // User not allowed to enroll
+ SoapErrorAuthorization = "s:authorization"
+
+ // Failed to get certificate
+ SoapErrorCertificateRequest = "s:certificaterequest"
+
+ // Generic failure from management server, such as a database access error
+ SoapErrorEnrollmentServer = "s:enrollmentserver"
+
+ // The server hit an unexpected issue
+ SoapErrorInternalServiceFault = "s:internalservicefault"
+
+ // Cannot parse the security header
+ SoapErrorInvalidSecurity = "a:invalidsecurity"
+)
+
+// MS-MDE2 Message constants
+const (
+ // Minimum supported version
+ MinEnrollmentVersion = "4.0"
+
+ // Maximum supported version
+ MaxEnrollmentVersion = "5.0"
+
+ // xsi:nil indicates value is not present
+ DefaultStateXSI = "true"
+
+ // Supported authentication types
+ AuthOnPremise = "OnPremise"
+
+ // SOAP Fault codes
+ SoapFaultRecv = "s:receiver"
+
+ // SOAP Fault default error locale
+ SoapFaultLocale = "en-us"
+
+ // HTTP Content Type for SOAP responses
+ SoapContentType = "application/soap+xml; charset=utf-8"
+)
+
+func ResolveMicrosoftMDMDiscovery(serverURL string) (string, error) {
+ return commonmdm.ResolveURL(serverURL, MDE2DiscoveryPath, false)
+}
+
+func ResolveMicrosoftMDMPolicy(serverURL string) (string, error) {
+ return commonmdm.ResolveURL(serverURL, MDE2PolicyPath, false)
+}
+
+func ResolveMicrosoftMDMEnroll(serverURL string) (string, error) {
+ return commonmdm.ResolveURL(serverURL, MDE2EnrollPath, false)
+}
diff --git a/server/mdm/windows/windows_mdm.go b/server/mdm/windows/windows_mdm.go
deleted file mode 100644
index b61f88100..000000000
--- a/server/mdm/windows/windows_mdm.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package windows_mdm
-
-import "github.com/fleetdm/fleet/v4/server/mdm/internal/commonmdm"
-
-const (
- // DiscoveryPath is Fleet's HTTP path for the Windows MDM Discovery endpoint.
- DiscoveryPath = "/EnrollmentServer/Discovery.svc"
-)
-
-func ResolveWindowsMDMDiscovery(serverURL string) (string, error) {
- return commonmdm.ResolveURL(serverURL, DiscoveryPath, false)
-}
diff --git a/server/service/appconfig.go b/server/service/appconfig.go
index 830149182..4be0f4cda 100644
--- a/server/service/appconfig.go
+++ b/server/service/appconfig.go
@@ -52,11 +52,11 @@ type appConfigResponseFields struct {
// MDMEnabled is true if fleet serve was started with
// FLEET_DEV_MDM_ENABLED=1.
//
- // Undocumented feature flag for Windows MDM, used to determine if the
- // Windows MDM feature is visible in the UI and can be enabled. More details
+ // Undocumented feature flag for Microsoft MDM, used to determine if the
+ // Microsoft MDM feature is visible in the UI and can be enabled. More details
// here: https://github.com/fleetdm/fleet/issues/12257
//
- // TODO: remove this flag once the Windows MDM feature is ready for
+ // TODO: remove this flag once the Microsoft MDM feature is ready for
// release.
MDMEnabled bool `json:"mdm_enabled,omitempty"`
}
@@ -648,7 +648,7 @@ func (svc *Service) validateMDM(
// Windows validation
if !config.IsMDMFeatureFlagEnabled() {
- if mdm.WindowsEnabledAndConfigured {
+ if mdm.MicrosoftEnabledAndConfigured {
invalid.Append("mdm.windows_enabled_and_configured", "cannot enable Windows MDM without the feature flag explicitly enabled")
return
}
diff --git a/server/service/handler.go b/server/service/handler.go
index 0a210919d..8187cff62 100644
--- a/server/service/handler.go
+++ b/server/service/handler.go
@@ -37,6 +37,8 @@ import (
"github.com/throttled/throttled/v2"
"go.elastic.co/apm/module/apmgorilla/v2"
otmiddleware "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
+
+ microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
)
type errorHandler struct {
@@ -443,52 +445,52 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// endpoint that's behind the mdmConfiguredMiddleware, this applies
// both to this set of endpoints and to any public/token-authenticated
// endpoints using `neMDM` below in this file.
- mdmConfiguredMiddleware := mdmconfigured.NewAppleMiddleware(svc)
- mdm := ue.WithCustomMiddleware(mdmConfiguredMiddleware.Verify())
- mdm.POST("/api/_version_/fleet/mdm/apple/enqueue", enqueueMDMAppleCommandEndpoint, enqueueMDMAppleCommandRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/commandresults", getMDMAppleCommandResultsEndpoint, getMDMAppleCommandResultsRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/commands", listMDMAppleCommandsEndpoint, listMDMAppleCommandsRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/filevault/summary", getMdmAppleFileVaultSummaryEndpoint, getMDMAppleFileVaultSummaryRequest{})
- mdm.POST("/api/_version_/fleet/mdm/apple/profiles", newMDMAppleConfigProfileEndpoint, newMDMAppleConfigProfileRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesEndpoint, listMDMAppleConfigProfilesRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", getMDMAppleConfigProfileEndpoint, getMDMAppleConfigProfileRequest{})
- mdm.DELETE("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", deleteMDMAppleConfigProfileEndpoint, deleteMDMAppleConfigProfileRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/profiles/summary", getMDMAppleProfilesSummaryEndpoint, getMDMAppleProfilesSummaryRequest{})
- mdm.POST("/api/_version_/fleet/mdm/apple/enrollment_profile", createMDMAppleSetupAssistantEndpoint, createMDMAppleSetupAssistantRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/enrollment_profile", getMDMAppleSetupAssistantEndpoint, getMDMAppleSetupAssistantRequest{})
- mdm.DELETE("/api/_version_/fleet/mdm/apple/enrollment_profile", deleteMDMAppleSetupAssistantEndpoint, deleteMDMAppleSetupAssistantRequest{})
+ mdmConfiguredMiddleware := mdmconfigured.NewMDMConfigMiddleware(svc)
+ mdmAppleMW := ue.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleMDM())
+ mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/enqueue", enqueueMDMAppleCommandEndpoint, enqueueMDMAppleCommandRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/commandresults", getMDMAppleCommandResultsEndpoint, getMDMAppleCommandResultsRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/commands", listMDMAppleCommandsEndpoint, listMDMAppleCommandsRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/filevault/summary", getMdmAppleFileVaultSummaryEndpoint, getMDMAppleFileVaultSummaryRequest{})
+ mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/profiles", newMDMAppleConfigProfileEndpoint, newMDMAppleConfigProfileRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesEndpoint, listMDMAppleConfigProfilesRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", getMDMAppleConfigProfileEndpoint, getMDMAppleConfigProfileRequest{})
+ mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", deleteMDMAppleConfigProfileEndpoint, deleteMDMAppleConfigProfileRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles/summary", getMDMAppleProfilesSummaryEndpoint, getMDMAppleProfilesSummaryRequest{})
+ mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/enrollment_profile", createMDMAppleSetupAssistantEndpoint, createMDMAppleSetupAssistantRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/enrollment_profile", getMDMAppleSetupAssistantEndpoint, getMDMAppleSetupAssistantRequest{})
+ mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/enrollment_profile", deleteMDMAppleSetupAssistantEndpoint, deleteMDMAppleSetupAssistantRequest{})
// TODO: are those undocumented endpoints still needed? I think they were only used
// by 'fleetctl apple-mdm' sub-commands.
- mdm.POST("/api/_version_/fleet/mdm/apple/installers", uploadAppleInstallerEndpoint, uploadAppleInstallerRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/installers/{installer_id:[0-9]+}", getAppleInstallerEndpoint, getAppleInstallerDetailsRequest{})
- mdm.DELETE("/api/_version_/fleet/mdm/apple/installers/{installer_id:[0-9]+}", deleteAppleInstallerEndpoint, deleteAppleInstallerDetailsRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/installers", listMDMAppleInstallersEndpoint, listMDMAppleInstallersRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/devices", listMDMAppleDevicesEndpoint, listMDMAppleDevicesRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/dep/devices", listMDMAppleDEPDevicesEndpoint, listMDMAppleDEPDevicesRequest{})
+ mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/installers", uploadAppleInstallerEndpoint, uploadAppleInstallerRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/installers/{installer_id:[0-9]+}", getAppleInstallerEndpoint, getAppleInstallerDetailsRequest{})
+ mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/installers/{installer_id:[0-9]+}", deleteAppleInstallerEndpoint, deleteAppleInstallerDetailsRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/installers", listMDMAppleInstallersEndpoint, listMDMAppleInstallersRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/devices", listMDMAppleDevicesEndpoint, listMDMAppleDevicesRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/dep/devices", listMDMAppleDEPDevicesEndpoint, listMDMAppleDEPDevicesRequest{})
// bootstrap-package routes
- mdm.POST("/api/_version_/fleet/mdm/apple/bootstrap", uploadBootstrapPackageEndpoint, uploadBootstrapPackageRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/bootstrap/{team_id:[0-9]+}/metadata", bootstrapPackageMetadataEndpoint, bootstrapPackageMetadataRequest{})
- mdm.DELETE("/api/_version_/fleet/mdm/apple/bootstrap/{team_id:[0-9]+}", deleteBootstrapPackageEndpoint, deleteBootstrapPackageRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/bootstrap/summary", getMDMAppleBootstrapPackageSummaryEndpoint, getMDMAppleBootstrapPackageSummaryRequest{})
+ mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/bootstrap", uploadBootstrapPackageEndpoint, uploadBootstrapPackageRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/bootstrap/{team_id:[0-9]+}/metadata", bootstrapPackageMetadataEndpoint, bootstrapPackageMetadataRequest{})
+ mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/bootstrap/{team_id:[0-9]+}", deleteBootstrapPackageEndpoint, deleteBootstrapPackageRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/bootstrap/summary", getMDMAppleBootstrapPackageSummaryEndpoint, getMDMAppleBootstrapPackageSummaryRequest{})
// host-specific mdm routes
- mdm.PATCH("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/unenroll", mdmAppleCommandRemoveEnrollmentProfileEndpoint, mdmAppleCommandRemoveEnrollmentProfileRequest{})
- mdm.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
- mdm.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/lock", deviceLockEndpoint, deviceLockRequest{})
- mdm.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/wipe", deviceWipeEndpoint, deviceWipeRequest{})
+ mdmAppleMW.PATCH("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/unenroll", mdmAppleCommandRemoveEnrollmentProfileEndpoint, mdmAppleCommandRemoveEnrollmentProfileRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
+ mdmAppleMW.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/lock", deviceLockEndpoint, deviceLockRequest{})
+ mdmAppleMW.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/wipe", deviceWipeEndpoint, deviceWipeRequest{})
- mdm.PATCH("/api/_version_/fleet/mdm/apple/settings", updateMDMAppleSettingsEndpoint, updateMDMAppleSettingsRequest{})
- mdm.PATCH("/api/_version_/fleet/mdm/apple/setup", updateMDMAppleSetupEndpoint, updateMDMAppleSetupRequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple", getAppleMDMEndpoint, nil)
+ mdmAppleMW.PATCH("/api/_version_/fleet/mdm/apple/settings", updateMDMAppleSettingsEndpoint, updateMDMAppleSettingsRequest{})
+ mdmAppleMW.PATCH("/api/_version_/fleet/mdm/apple/setup", updateMDMAppleSetupEndpoint, updateMDMAppleSetupRequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple", getAppleMDMEndpoint, nil)
- mdm.POST("/api/_version_/fleet/mdm/apple/setup/eula", createMDMAppleEULAEndpoint, createMDMAppleEULARequest{})
- mdm.GET("/api/_version_/fleet/mdm/apple/setup/eula/metadata", getMDMAppleEULAMetadataEndpoint, getMDMAppleEULAMetadataRequest{})
- mdm.DELETE("/api/_version_/fleet/mdm/apple/setup/eula/{token}", deleteMDMAppleEULAEndpoint, deleteMDMAppleEULARequest{})
+ mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/setup/eula", createMDMAppleEULAEndpoint, createMDMAppleEULARequest{})
+ mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/setup/eula/metadata", getMDMAppleEULAMetadataEndpoint, getMDMAppleEULAMetadataRequest{})
+ mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/setup/eula/{token}", deleteMDMAppleEULAEndpoint, deleteMDMAppleEULARequest{})
- mdm.POST("/api/_version_/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileEndpoint, preassignMDMAppleProfileRequest{})
- mdm.POST("/api/_version_/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentEndpoint, matchMDMApplePreassignmentRequest{})
+ mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileEndpoint, preassignMDMAppleProfileRequest{})
+ mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentEndpoint, matchMDMApplePreassignmentRequest{})
// the following set of mdm endpoints must always be accessible (even
// if MDM is not configured) as it bootstraps the setup of MDM
@@ -531,7 +533,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
).GET("/api/_version_/fleet/device/{token}/transparency", transparencyURL, transparencyURLRequest{})
// mdm-related endpoints available via device authentication
- demdm := de.WithCustomMiddleware(mdmConfiguredMiddleware.Verify())
+ demdm := de.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleMDM())
demdm.WithCustomMiddleware(
errorLimiter.Limit("get_device_mdm", desktopQuota),
).GET("/api/_version_/fleet/device/{token}/mdm/apple/manual_enrollment_profile", getDeviceMDMManualEnrollProfileEndpoint, getDeviceMDMManualEnrollProfileRequest{})
@@ -583,13 +585,20 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// `service.mdmAppleConfigurationRequiredEndpoints` when you add an
// endpoint that's behind the mdmConfiguredMiddleware, this applies
// both to this set of endpoints and to any user authenticated
- // endpoints using `mdm.*` above in this file.
- neMDM := ne.WithCustomMiddleware(mdmConfiguredMiddleware.Verify())
- neMDM.GET(apple_mdm.EnrollPath, mdmAppleEnrollEndpoint, mdmAppleEnrollRequest{})
- neMDM.GET(apple_mdm.InstallerPath, mdmAppleGetInstallerEndpoint, mdmAppleGetInstallerRequest{})
- neMDM.HEAD(apple_mdm.InstallerPath, mdmAppleHeadInstallerEndpoint, mdmAppleHeadInstallerRequest{})
- neMDM.GET("/api/_version_/fleet/mdm/apple/bootstrap", downloadBootstrapPackageEndpoint, downloadBootstrapPackageRequest{})
- neMDM.GET("/api/_version_/fleet/mdm/apple/setup/eula/{token}", getMDMAppleEULAEndpoint, getMDMAppleEULARequest{})
+ // endpoints using `mdmAppleMW.*` above in this file.
+ neAppleMDM := ne.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleMDM())
+ neAppleMDM.GET(apple_mdm.EnrollPath, mdmAppleEnrollEndpoint, mdmAppleEnrollRequest{})
+ neAppleMDM.GET(apple_mdm.InstallerPath, mdmAppleGetInstallerEndpoint, mdmAppleGetInstallerRequest{})
+ neAppleMDM.HEAD(apple_mdm.InstallerPath, mdmAppleHeadInstallerEndpoint, mdmAppleHeadInstallerRequest{})
+ neAppleMDM.GET("/api/_version_/fleet/mdm/apple/bootstrap", downloadBootstrapPackageEndpoint, downloadBootstrapPackageRequest{})
+ neAppleMDM.GET("/api/_version_/fleet/mdm/apple/setup/eula/{token}", getMDMAppleEULAEndpoint, getMDMAppleEULARequest{})
+
+ // These endpoint are used by Microsoft devices during MDM device enrollment phase
+ neMicrosoftMDM := ne.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyMicrosoftMDM())
+
+ // Microsoft Device Enrollment Discovery Endpoint
+ // This endpoint is unauthenticated and is used by Microsoft devices to discover the MDM server
+ neMicrosoftMDM.POST(microsoft_mdm.MDE2DiscoveryPath, mdmMicrosoftDiscoveryEndpoint, SoapRequest{})
ne.POST("/api/fleet/orbit/enroll", enrollOrbitEndpoint, EnrollOrbitRequest{})
@@ -640,10 +649,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
errorLimiter.Limit("ping_orbit", desktopQuota),
).HEAD("/api/fleet/orbit/ping", orbitPingEndpoint, orbitPingRequest{})
- neMDM.WithCustomMiddleware(limiter.Limit("login", throttled.RateQuota{MaxRate: loginRateLimit, MaxBurst: 9})).
+ neAppleMDM.WithCustomMiddleware(limiter.Limit("login", throttled.RateQuota{MaxRate: loginRateLimit, MaxBurst: 9})).
POST("/api/_version_/fleet/mdm/sso", initiateMDMAppleSSOEndpoint, initiateMDMAppleSSORequest{})
- neMDM.WithCustomMiddleware(limiter.Limit("login", throttled.RateQuota{MaxRate: loginRateLimit, MaxBurst: 9})).
+ neAppleMDM.WithCustomMiddleware(limiter.Limit("login", throttled.RateQuota{MaxRate: loginRateLimit, MaxBurst: 9})).
POST("/api/_version_/fleet/mdm/sso/callback", callbackMDMAppleSSOEndpoint, callbackMDMAppleSSORequest{})
}
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index ee3aae4f0..1e5000d22 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -4876,7 +4876,7 @@ func (s *integrationTestSuite) TestAppConfig() {
"mdm": { "apple_bm_default_team": "xyz" }
}`), http.StatusUnprocessableEntity, &acResp)
- // try to enable windows mdm, impossible without the feature flag
+ // try to enable Microsoft mdm, impossible without the feature flag
// (only set in mdm integrations tests)
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "windows_enabled_and_configured": true }
diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go
index ec09fbfde..127df859d 100644
--- a/server/service/integration_enterprise_test.go
+++ b/server/service/integration_enterprise_test.go
@@ -2491,6 +2491,7 @@ func (s *integrationEnterpriseTestSuite) TestAppleMDMNotConfigured() {
if route.deviceAuthenticated {
path = fmt.Sprintf(route.path, tkn)
}
+
res := s.Do(route.method, path, nil, expectedErr.StatusCode())
errMsg := extractServerErrorText(res.Body)
assert.Contains(t, errMsg, expectedErr.Error())
@@ -2793,13 +2794,13 @@ func (s *integrationEnterpriseTestSuite) TestOrbitConfigNudgeSettings() {
// missing orbit key
s.DoJSON("POST", "/api/fleet/orbit/config", nil, http.StatusUnauthorized, &resp)
- // nudge config is empty if macos_updates is not set, and windows mdm notifications are unset
+ // nudge config is empty if macos_updates is not set, and Microsoft mdm notifications are unset
h := createOrbitEnrolledHost(t, "darwin", "h", s.ds)
resp = orbitGetConfigResponse{}
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp)
require.Empty(t, resp.NudgeConfig)
- require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment)
- require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint)
+ require.False(t, resp.Notifications.NeedsProgrammaticMicrosoftMDMEnrollment)
+ require.Empty(t, resp.Notifications.MicrosoftMDMDiscoveryEndpoint)
// set macos_updates
s.applyConfig([]byte(`
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index 490fb362f..72e5870b8 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -6,6 +6,7 @@ import (
"crypto/x509"
"encoding/base64"
"encoding/json"
+ "encoding/xml"
"errors"
"fmt"
"io"
@@ -15,6 +16,7 @@ import (
"net/url"
"os"
"path/filepath"
+ "regexp"
"sort"
"strconv"
"strings"
@@ -30,7 +32,7 @@ import (
"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"
- windows_mdm "github.com/fleetdm/fleet/v4/server/mdm/windows"
+ microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/mock"
"github.com/fleetdm/fleet/v4/server/service/schedule"
@@ -227,8 +229,8 @@ func (s *integrationMDMTestSuite) TearDownTest() {
"mdm": { "macos_settings": { "enable_disk_encryption": false } }
}`), http.StatusOK)
}
- if appCfg.MDM.WindowsEnabledAndConfigured {
- // ensure windows MDM is disabled on exit
+ if appCfg.MDM.MicrosoftEnabledAndConfigured {
+ // ensure microsoft MDM is disabled on exit
s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "windows_enabled_and_configured": false }
}`), http.StatusOK)
@@ -1730,83 +1732,6 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() {
s.DoJSON("DELETE", deletePath, nil, http.StatusBadRequest, &deleteResp)
}
-func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() {
- ctx := context.Background()
- t := s.T()
-
- // the feature flag is enabled for the MDM test suite
- var acResp appConfigResponse
- s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
- assert.True(t, acResp.MDMEnabled)
- assert.False(t, acResp.MDM.WindowsEnabledAndConfigured)
-
- // create a couple teams
- tm1, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "1"})
- require.NoError(t, err)
- tm2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "2"})
- require.NoError(t, err)
-
- // create some hosts - a Windows workstation in each team and no-team,
- // Windows server in no team, Windows workstation enrolled in a 3rd-party in
- // team 2, Windows workstation already enrolled in Fleet in no team, and a
- // macOS host in no team.
- metadataHosts := []struct {
- os string
- suffix string
- isServer bool
- teamID *uint
- enrolledName string
- shouldEnroll bool
- }{
- {"windows", "win-no-team", false, nil, "", true},
- {"windows", "win-team-1", false, &tm1.ID, "", true},
- {"windows", "win-team-2", false, &tm2.ID, "", true},
- {"windows", "win-server", true, nil, "", false}, // is a server
- {"windows", "win-third-party", false, &tm2.ID, fleet.WellKnownMDMSimpleMDM, false}, // is enrolled in 3rd-party
- {"windows", "win-fleet", false, nil, fleet.WellKnownMDMFleet, false}, // is already Fleet-enrolled
- {"darwin", "macos-no-team", false, nil, "", false}, // is not Windows
- }
- hostsBySuffix := make(map[string]*fleet.Host, len(metadataHosts))
- for _, meta := range metadataHosts {
- h := createOrbitEnrolledHost(t, meta.os, meta.suffix, s.ds)
- createDeviceTokenForHost(t, s.ds, h.ID, meta.suffix)
- err := s.ds.SetOrUpdateMDMData(ctx, h.ID, meta.isServer, meta.enrolledName != "", "https://example.com", false, meta.enrolledName)
- require.NoError(t, err)
- if meta.teamID != nil {
- err = s.ds.AddHostsToTeam(ctx, meta.teamID, []uint{h.ID})
- require.NoError(t, err)
- }
- hostsBySuffix[meta.suffix] = h
- }
-
- // enable Windows MDM
- s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
- "mdm": { "windows_enabled_and_configured": true }
- }`), http.StatusOK, &acResp)
- assert.True(t, acResp.MDM.WindowsEnabledAndConfigured)
-
- // get the orbit config for each host, verify that only the expected ones
- // receive the "needs enrollment to Windows MDM" notification.
- for _, meta := range metadataHosts {
- var resp orbitGetConfigResponse
- s.DoJSON("POST", "/api/fleet/orbit/config",
- json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)),
- http.StatusOK, &resp)
- require.Equal(t, meta.shouldEnroll, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment)
- if meta.shouldEnroll {
- require.Contains(t, resp.Notifications.WindowsMDMDiscoveryEndpoint, windows_mdm.DiscoveryPath)
- } else {
- require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint)
- }
- }
-
- // disable Windows MDM
- s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
- "mdm": { "windows_enabled_and_configured": false }
- }`), http.StatusOK, &acResp)
- assert.False(t, acResp.MDM.WindowsEnabledAndConfigured)
-}
-
func (s *integrationMDMTestSuite) TestAppConfigMDMAppleProfiles() {
t := s.T()
@@ -5088,6 +5013,196 @@ func (s *integrationMDMTestSuite) TestMDMMigration() {
checkMigrationResponses(host, token)
}
+// ///////////////////////////////////////////////////////////////////////////
+// Microsoft MDM tests
+
+func (s *integrationMDMTestSuite) TestAppConfigMicrosoftMDM() {
+ ctx := context.Background()
+ t := s.T()
+
+ // the feature flag is enabled for the MDM test suite
+ var acResp appConfigResponse
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ assert.True(t, acResp.MDMEnabled)
+ assert.False(t, acResp.MDM.MicrosoftEnabledAndConfigured)
+
+ // create a couple teams
+ tm1, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "1"})
+ require.NoError(t, err)
+ tm2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "2"})
+ require.NoError(t, err)
+
+ // create some hosts - a Windows workstation in each team and no-team,
+ // Windows server in no team, Windows workstation enrolled in a 3rd-party in
+ // team 2, Windows workstation already enrolled in Fleet in no team, and a
+ // macOS host in no team.
+ metadataHosts := []struct {
+ os string
+ suffix string
+ isServer bool
+ teamID *uint
+ enrolledName string
+ shouldEnroll bool
+ }{
+ {"windows", "win-no-team", false, nil, "", true},
+ {"windows", "win-team-1", false, &tm1.ID, "", true},
+ {"windows", "win-team-2", false, &tm2.ID, "", true},
+ {"windows", "win-server", true, nil, "", false}, // is a server
+ {"windows", "win-third-party", false, &tm2.ID, fleet.WellKnownMDMSimpleMDM, false}, // is enrolled in 3rd-party
+ {"windows", "win-fleet", false, nil, fleet.WellKnownMDMFleet, false}, // is already Fleet-enrolled
+ {"darwin", "macos-no-team", false, nil, "", false}, // is not Windows
+ }
+ hostsBySuffix := make(map[string]*fleet.Host, len(metadataHosts))
+ for _, meta := range metadataHosts {
+ h := createOrbitEnrolledHost(t, meta.os, meta.suffix, s.ds)
+ createDeviceTokenForHost(t, s.ds, h.ID, meta.suffix)
+ err := s.ds.SetOrUpdateMDMData(ctx, h.ID, meta.isServer, meta.enrolledName != "", "https://example.com", false, meta.enrolledName)
+ require.NoError(t, err)
+ if meta.teamID != nil {
+ err = s.ds.AddHostsToTeam(ctx, meta.teamID, []uint{h.ID})
+ require.NoError(t, err)
+ }
+ hostsBySuffix[meta.suffix] = h
+ }
+
+ // enable Microsoft MDM
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
+ "mdm": { "windows_enabled_and_configured": true }
+ }`), http.StatusOK, &acResp)
+ assert.True(t, acResp.MDM.MicrosoftEnabledAndConfigured)
+
+ // get the orbit config for each host, verify that only the expected ones
+ // receive the "needs enrollment to Microsoft MDM" notification.
+ for _, meta := range metadataHosts {
+ var resp orbitGetConfigResponse
+ s.DoJSON("POST", "/api/fleet/orbit/config",
+ json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)),
+ http.StatusOK, &resp)
+ require.Equal(t, meta.shouldEnroll, resp.Notifications.NeedsProgrammaticMicrosoftMDMEnrollment)
+ if meta.shouldEnroll {
+ require.Contains(t, resp.Notifications.MicrosoftMDMDiscoveryEndpoint, microsoft_mdm.MDE2DiscoveryPath)
+ } else {
+ require.Empty(t, resp.Notifications.MicrosoftMDMDiscoveryEndpoint)
+ }
+ }
+
+ // disable Microsoft MDM
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
+ "mdm": { "windows_enabled_and_configured": false }
+ }`), http.StatusOK, &acResp)
+ assert.False(t, acResp.MDM.MicrosoftEnabledAndConfigured)
+}
+
+func (s *integrationMDMTestSuite) TestValidDiscoveryRequest() {
+ appConf, err := s.ds.AppConfig(context.Background())
+ require.NoError(s.T(), err)
+ appConf.MDM.MicrosoftEnabledAndConfigured = true
+ err = s.ds.SaveAppConfig(context.Background(), appConf)
+ require.NoError(s.T(), err)
+
+ // Preparing the Discovery Request message
+ requestBytes := []byte(`
+