Pushing initial support for MS-MDE2 Discovery message (#12387)

This PR requires the Windows MDM configuration changes - This will be
updated next week

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- [x] Documented any permissions changes
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
  - For Orbit and Fleet Desktop changes:
This commit is contained in:
Marcos Oviedo 2023-06-22 17:31:17 -03:00 committed by GitHub
parent e95e075e77
commit 22bb16bf2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1750 additions and 193 deletions

View File

@ -0,0 +1 @@
* Microsoft MDM Enrollment Protocol: Added support for the DiscoveryRequest messages

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() &&

View File

@ -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:"-"`
}

View File

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

View File

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

View File

@ -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)
}

View File

@ -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)
}

View File

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

View File

@ -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{})
}

View File

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

View File

@ -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(`

View File

@ -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(`
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover</a:Action>
<a:MessageID>urn:uuid:148132ec-a575-4322-b01b-6172a9cf8478</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">https://mdmwindows.com:443/EnrollmentServer/Discovery.svc</a:To>
</s:Header>
<s:Body>
<Discover xmlns="http://schemas.microsoft.com/windows/management/2012/01/enrollment">
<request xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<EmailAddress>demo@mdmwindows.com</EmailAddress>
<RequestVersion>5.0</RequestVersion>
<DeviceType>CIMClient_Windows</DeviceType>
<ApplicationVersion>6.2.9200.2965</ApplicationVersion>
<OSEdition>48</OSEdition>
<AuthPolicies>
<AuthPolicy>OnPremise</AuthPolicy>
<AuthPolicy>Federated</AuthPolicy>
</AuthPolicies>
</request>
</Discover>
</s:Body>
</s:Envelope>`)
resp := s.DoRaw("POST", microsoft_mdm.MDE2DiscoveryPath, requestBytes, http.StatusOK)
resBytes, err := io.ReadAll(resp.Body)
require.NoError(s.T(), err)
require.Contains(s.T(), resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
// Checking if SOAP response can be unmarshalled to an golang type
var xmlType interface{}
err = xml.Unmarshal(resBytes, &xmlType)
require.NoError(s.T(), err)
// Checking if SOAP response contains a valid DiscoveryResponse message
resSoapMsg := string(resBytes)
require.True(s.T(), s.isXMLTagContentPresent("AuthPolicy", resSoapMsg))
require.True(s.T(), s.isXMLTagContentPresent("EnrollmentVersion", resSoapMsg))
require.True(s.T(), s.isXMLTagContentPresent("EnrollmentPolicyServiceUrl", resSoapMsg))
require.True(s.T(), s.isXMLTagContentPresent("EnrollmentServiceUrl", resSoapMsg))
}
func (s *integrationMDMTestSuite) TestInvalidDiscoveryRequest() {
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(`
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover</a:Action>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">https://mdmwindows.com:443/EnrollmentServer/Discovery.svc</a:To>
</s:Header>
<s:Body>
<Discover xmlns="http://schemas.microsoft.com/windows/management/2012/01/enrollment">
<request xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<EmailAddress>demo@mdmwindows.com</EmailAddress>
<RequestVersion>5.0</RequestVersion>
<DeviceType>CIMClient_Windows</DeviceType>
<ApplicationVersion>6.2.9200.2965</ApplicationVersion>
<OSEdition>48</OSEdition>
<AuthPolicies>
<AuthPolicy>OnPremise</AuthPolicy>
<AuthPolicy>Federated</AuthPolicy>
</AuthPolicies>
</request>
</Discover>
</s:Body>
</s:Envelope>`)
resp := s.DoRaw("POST", microsoft_mdm.MDE2DiscoveryPath, requestBytes, http.StatusOK)
resBytes, err := io.ReadAll(resp.Body)
require.NoError(s.T(), err)
require.Contains(s.T(), resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
// Checking if response can be unmarshalled to an golang type
var xmlType interface{}
err = xml.Unmarshal(resBytes, &xmlType)
require.NoError(s.T(), err)
// Checking if SOAP response contains a valid SoapFault message
resSoapMsg := string(resBytes)
require.True(s.T(), s.isXMLTagContentPresent("s:value", resSoapMsg))
require.True(s.T(), s.isXMLTagContentPresent("s:text", resSoapMsg))
}
// ///////////////////////////////////////////////////////////////////////////
// Common helpers
func (s *integrationMDMTestSuite) runWorker() {
err := s.worker.ProcessJobs(context.Background())
require.NoError(s.T(), err)
@ -5095,3 +5210,13 @@ func (s *integrationMDMTestSuite) runWorker() {
require.NoError(s.T(), err)
require.Empty(s.T(), pending)
}
func (s *integrationMDMTestSuite) isXMLTagContentPresent(xmlTag string, payload string) bool {
regex := fmt.Sprintf("<%s.*>(.+)</%s.*>", xmlTag, xmlTag)
matched, err := regexp.MatchString(regex, payload)
if err != nil {
return false
}
return matched
}

View File

@ -382,3 +382,25 @@ func (svc *Service) MDMAppleDeleteEULA(ctx context.Context, token string) error
return fleet.ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
// Microsoft MDM Middleware
////////////////////////////////////////////////////////////////////////////////
func (svc *Service) VerifyMDMMicrosoftConfigured(ctx context.Context) error {
appCfg, err := svc.ds.AppConfig(ctx)
if err != nil {
// skipauth: Authorization is currently for user endpoints only.
svc.authz.SkipAuthorization(ctx)
return err
}
// Microsoft MDM configuration setting
if !appCfg.MDM.MicrosoftEnabledAndConfigured {
// skipauth: Authorization is currently for user endpoints only.
svc.authz.SkipAuthorization(ctx)
return fleet.ErrMDMNotConfigured
}
return nil
}

View File

@ -129,3 +129,51 @@ func TestVerifyMDMAppleConfigured(t *testing.T) {
ds.AppConfigFuncInvoked = false
require.False(t, authzCtx.Checked())
}
// TODO: update this test with the correct config option
func TestVerifyMDMMicrosoftConfigured(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
cfg := config.TestConfig()
svc, baseCtx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
// mdm not configured
authzCtx := &authz_ctx.AuthorizationContext{}
ctx := authz_ctx.NewContext(baseCtx, authzCtx)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: false}}, nil
}
err := svc.VerifyMDMMicrosoftConfigured(ctx)
require.ErrorIs(t, err, fleet.ErrMDMNotConfigured)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.True(t, authzCtx.Checked())
// error retrieving app config
authzCtx = &authz_ctx.AuthorizationContext{}
ctx = authz_ctx.NewContext(baseCtx, authzCtx)
testErr := errors.New("test err")
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return nil, testErr
}
err = svc.VerifyMDMMicrosoftConfigured(ctx)
require.ErrorIs(t, err, testErr)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.True(t, authzCtx.Checked())
// mdm configured
authzCtx = &authz_ctx.AuthorizationContext{}
ctx = authz_ctx.NewContext(baseCtx, authzCtx)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{MicrosoftEnabledAndConfigured: true}}, nil
}
err = svc.VerifyMDMMicrosoftConfigured(ctx)
require.NoError(t, err)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.False(t, authzCtx.Checked())
}

View File

@ -0,0 +1,631 @@
package service
import (
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/fleet"
mdm_types "github.com/fleetdm/fleet/v4/server/fleet"
mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/google/uuid"
)
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// MS-MDE2 XML types used by the SOAP protocol
// MS-MDE2 is a client-to-server protocol that consists of a SOAP-based Web service.
// SOAP is a lightweight and XML based protocol that consists of three parts:
// - An envelope that defines a framework for describing what is in a message and how to process it
// - A set of encoding rules for expressing instances of application-defined datatypes
// - And a convention for representing remote procedure calls and responses.
// SoapResponse is the Soap Envelope Response type for MS-MDE2 responses from the server
// This envelope XML message is composed by a mandatory SOAP envelope, a SOAP header, and a SOAP body
type SoapResponse struct {
XMLName xml.Name `xml:"s:Envelope"`
XmlNSS string `xml:"xmlns:s,attr"`
XmlNSA string `xml:"xmlns:a,attr"`
XmlNSU *string `xml:"xmlns:u,attr,omitempty"`
Header mdm_types.ResponseHeader `xml:"s:Header"`
Body mdm_types.BodyResponse `xml:"s:Body"`
}
// error returns soap fault error if present
func (msg SoapResponse) error() error {
// placeholder to log SoapFault error message if needed
// msg.Body.SoapFault != nil
return nil
}
// hijackRender writes the response header and the RAW XML output
func (msg SoapResponse) hijackRender(ctx context.Context, w http.ResponseWriter) {
xmlRes, err := xml.MarshalIndent(msg, "", "\t")
if err != nil {
logging.WithExtras(ctx, "microsoft MDM SoapResponse", err)
w.WriteHeader(http.StatusBadRequest)
return
}
xmlRes = append(xmlRes, '\n')
w.Header().Set("Content-Type", mdm.SoapContentType)
w.Header().Set("Content-Length", strconv.Itoa(len(xmlRes)))
w.WriteHeader(http.StatusOK)
if n, err := w.Write(xmlRes); err != nil {
logging.WithExtras(ctx, "err", err, "written", n)
}
}
// getMessageID returns the message ID from the header
func (req *SoapRequest) getMessageID() string {
return req.Header.MessageID
}
// isValidHeader checks for required fields in the header
func (req *SoapRequest) isValidHeader() error {
// Check for required fields
if len(req.XmlNSS) == 0 {
return errors.New("invalid SOAP header: XmlNSS")
}
if len(req.XmlNSA) == 0 {
return errors.New("invalid SOAP header: XmlNSA")
}
if len(req.Header.MessageID) == 0 {
return errors.New("invalid SOAP header: Header.MessageID")
}
if len(req.Header.Action.Content) == 0 {
return errors.New("invalid SOAP header: Header.Action")
}
if len(req.Header.ReplyTo.Address) == 0 {
return errors.New("invalid SOAP header: Header.ReplyTo")
}
if len(req.Header.To.Content) == 0 {
return errors.New("invalid SOAP header: Header.To")
}
return nil
}
// isValidBody checks for the presence of only one message
func (req *SoapRequest) isValidBody() error {
nonNilCount := 0
if req.Body.Discover != nil {
nonNilCount++
}
if req.Body.GetPolicies != nil {
nonNilCount++
}
if req.Body.RequestSecurityToken != nil {
nonNilCount++
}
if nonNilCount != 1 {
return errors.New("invalid SOAP body: Multiple messages or no message")
}
return nil
}
// IsValidDiscoveryMsg checks for required fields in the Discover message
func (req *SoapRequest) IsValidDiscoveryMsg() error {
if err := req.isValidHeader(); err != nil {
return fmt.Errorf("invalid discover message: %s", err)
}
if err := req.isValidBody(); err != nil {
return fmt.Errorf("invalid discover message: %s", err)
}
if req.Body.Discover == nil {
return errors.New("invalid discover message: Discover message not present")
}
if len(req.Body.Discover.XmlNS) == 0 {
return errors.New("invalid discover message: XmlNS")
}
// TODO: add check for valid email address
if len(req.Body.Discover.Request.EmailAddress) == 0 {
return errors.New("invalid discover message: Request.EmailAddress")
}
// Ensure that only valid versions are supported
if req.Body.Discover.Request.RequestVersion != mdm.MinEnrollmentVersion &&
req.Body.Discover.Request.RequestVersion != mdm.MaxEnrollmentVersion {
return errors.New("invalid discover message: Request.RequestVersion")
}
// Traverse the AuthPolicies slice and check for valid values
isInvalidAuth := true
for _, authPolicy := range req.Body.Discover.Request.AuthPolicies.AuthPolicy {
if authPolicy == mdm.AuthOnPremise {
isInvalidAuth = false
break
}
}
if isInvalidAuth {
return errors.New("invalid discover message: Request.AuthPolicies")
}
return nil
}
// IsValidGetPolicyMsg checks for required fields in the GetPolicies message
func (req *SoapRequest) IsValidGetPolicyMsg() error {
if err := req.isValidHeader(); err != nil {
return fmt.Errorf("invalid getpolicies message: %s", err)
}
if err := req.isValidBody(); err != nil {
return fmt.Errorf("invalid getpolicies message: %s", err)
}
if req.Body.GetPolicies == nil {
return errors.New("invalid getpolicies message: GetPolicies message not present")
}
if len(req.Body.GetPolicies.XmlNS) == 0 {
return errors.New("invalid getpolicies message: XmlNS")
}
return nil
}
// IsValidRequestSecurityTokenMsg checks for required fields in the RequestSecurityToken message
func (req *SoapRequest) IsValidRequestSecurityTokenMsg() error {
if err := req.isValidHeader(); err != nil {
return fmt.Errorf("invalid requestsecuritytoken message: %s", err)
}
if err := req.isValidBody(); err != nil {
return fmt.Errorf("invalid requestsecuritytoken message: %s", err)
}
if req.Body.RequestSecurityToken == nil {
return errors.New("invalid requestsecuritytoken message: RequestSecurityToken message not present")
}
if len(req.Body.RequestSecurityToken.TokenType) == 0 {
return errors.New("invalid requestsecuritytoken message: TokenType")
}
if len(req.Body.RequestSecurityToken.RequestType) == 0 {
return errors.New("invalid requestsecuritytoken message: RequestType")
}
if len(req.Body.RequestSecurityToken.BinarySecurityToken.ValueType) == 0 {
return errors.New("invalid requestsecuritytoken message: BinarySecurityToken.ValueType")
}
if len(req.Body.RequestSecurityToken.BinarySecurityToken.EncodingType) == 0 {
return errors.New("invalid requestsecuritytoken message: BinarySecurityToken.EncodingType")
}
if len(req.Body.RequestSecurityToken.BinarySecurityToken.Content) == 0 {
return errors.New("invalid requestsecuritytoken message: BinarySecurityToken.Content")
}
return nil
}
// SoapRequest is the Soap Envelope Request type for MS-MDE2 responses to the server
// This envelope XML message is composed by a mandatory SOAP envelope, a SOAP header, and a SOAP body
type SoapRequest struct {
XMLName xml.Name `xml:"Envelope"`
XmlNSS string `xml:"s,attr"`
XmlNSA string `xml:"a,attr"`
XmlNSU *string `xml:"u,attr,omitempty"`
XmlNSWsse *string `xml:"wsse,attr,omitempty"`
XmlNSWST *string `xml:"wst,attr,omitempty"`
XmlNSAC *string `xml:"ac,attr,omitempty"`
Header mdm_types.RequestHeader `xml:"Header"`
Body mdm_types.BodyRequest `xml:"Body"`
}
// getUtcTime returns the current timestamp plus the specified number of minutes,
// formatted as "2006-01-02T15:04:05.000Z".
func getUtcTime(minutes int) string {
// Get the current time and then add the specified number of minutes
now := time.Now()
future := now.Add(time.Duration(minutes) * time.Minute)
// Format and return the future time as a string
return future.UTC().Format("2006-01-02T15:04:05.000Z")
}
// NewDiscoverResponse creates a new DiscoverResponse struct based on the auth policy, policy url, and enrollment url
func NewDiscoverResponse(authPolicy string, policyUrl string, enrollmentUrl string) (mdm_types.DiscoverResponse, error) {
if (len(authPolicy) == 0) || (len(policyUrl) == 0) || (len(enrollmentUrl) == 0) {
return mdm_types.DiscoverResponse{}, errors.New("invalid parameters")
}
return mdm_types.DiscoverResponse{
XmlNS: mdm.DiscoverNS,
DiscoverResult: mdm_types.DiscoverResult{
AuthPolicy: authPolicy,
EnrollmentVersion: mdm.MinEnrollmentVersion,
EnrollmentPolicyServiceUrl: policyUrl,
EnrollmentServiceUrl: enrollmentUrl,
},
}, nil
}
// NewGetPoliciesResponse creates a new GetPoliciesResponse struct based on the minimal key length, certificate validity period, and renewal period
func NewGetPoliciesResponse(minimalKeyLength string, certificateValidityPeriodSeconds string, renewalPeriodSeconds string) (mdm_types.GetPoliciesResponse, error) {
if (len(minimalKeyLength) == 0) || (len(certificateValidityPeriodSeconds) == 0) || (len(renewalPeriodSeconds) == 0) {
return mdm_types.GetPoliciesResponse{}, errors.New("invalid parameters")
}
return mdm_types.GetPoliciesResponse{
XmlNS: mdm.PolicyNS,
Response: mdm_types.Response{
PolicyFriendlyName: mdm_types.ContentAttr{
Xsi: mdm.DefaultStateXSI,
XmlNS: mdm.EnrollXSI,
},
NextUpdateHours: mdm_types.ContentAttr{
Xsi: mdm.DefaultStateXSI,
XmlNS: mdm.EnrollXSI,
},
PoliciesNotChanged: mdm_types.ContentAttr{
Xsi: mdm.DefaultStateXSI,
XmlNS: mdm.EnrollXSI,
},
Policies: mdm_types.Policies{
Policy: mdm_types.GPPolicy{
PolicyOIDReference: "0",
CAs: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
Attributes: mdm_types.Attributes{
CommonName: "FleetDMAttributes",
PolicySchema: "3",
HashAlgorithmOIDReference: "0",
Revision: mdm_types.Revision{
MajorRevision: "101",
MinorRevision: "0",
},
CertificateValidity: mdm_types.CertificateValidity{
ValidityPeriodSeconds: certificateValidityPeriodSeconds,
RenewalPeriodSeconds: renewalPeriodSeconds,
},
Permission: mdm_types.Permission{
Enroll: "true",
AutoEnroll: "false",
},
SupersededPolicies: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
PrivateKeyFlags: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
SubjectNameFlags: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
EnrollmentFlags: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
GeneralFlags: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
RARequirements: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
KeyArchivalAttributes: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
Extensions: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
PrivateKeyAttributes: mdm_types.PrivateKeyAttributes{
MinimalKeyLength: minimalKeyLength,
KeySpec: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
KeyUsageProperty: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
Permissions: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
AlgorithmOIDReference: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
CryptoProviders: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
},
},
},
},
},
},
OIDs: mdm_types.OIDs{
// SHA1WithRSA encryption OID
// https://oidref.com/1.3.14.3.2.29
OID: mdm_types.OID{
Value: "1.3.14.3.2.29",
Group: "1",
OIDReferenceID: "0",
DefaultName: "szOID_NIST_sha256",
},
},
}, nil
}
// NewRequestSecurityTokenResponseCollection creates a new RequestSecurityTokenResponseCollection struct based on the provisioned token
func NewRequestSecurityTokenResponseCollection(provisionedToken string) (mdm_types.RequestSecurityTokenResponseCollection, error) {
if len(provisionedToken) == 0 {
return mdm_types.RequestSecurityTokenResponseCollection{}, errors.New("invalid parameters")
}
enrollSecExtVal := mdm.EnrollSecExt
return mdm_types.RequestSecurityTokenResponseCollection{
XmlNS: mdm.EnrollWSTrust,
RequestSecurityTokenResponse: mdm_types.RequestSecurityTokenResponse{
TokenType: mdm.EnrollTType,
DispositionMessage: mdm_types.SecAttr{
Content: "",
XmlNS: mdm.EnrollReq,
},
RequestID: mdm_types.SecAttr{
Content: "0",
XmlNS: mdm.EnrollReq,
},
RequestedSecurityToken: mdm_types.RequestedSecurityToken{
BinarySecurityToken: mdm_types.BinarySecurityToken{
Content: provisionedToken,
XmlNS: &enrollSecExtVal,
ValueType: mdm.EnrollPDoc,
EncodingType: mdm.EnrollEncode,
},
},
},
}, nil
}
// NewSoapFault creates a new SoapFault struct based on the error type, original message type, and error message
func NewSoapFault(errorType string, origMessage int, errorMessage error) mdm_types.SoapFault {
return mdm_types.SoapFault{
OriginalMessageType: origMessage,
Code: mdm_types.Code{
Value: mdm.SoapFaultRecv,
Subcode: mdm_types.Subcode{
Value: errorType,
},
},
Reason: mdm_types.Reason{
Text: mdm_types.ReasonText{
Content: errorMessage.Error(),
Lang: mdm.SoapFaultLocale,
},
},
}
}
// getSoapResponseFault Returns a SoapResponse with a SoapFault on its body
func getSoapResponseFault(relatesTo string, soapFault *mdm_types.SoapFault) errorer {
if len(relatesTo) == 0 {
relatesTo = "invalid_message_id"
}
soapResponse, _ := NewSoapResponse(soapFault, relatesTo)
return soapResponse
}
// NewSoapResponse creates a new SoapRequest struct based on the message type and the message content
func NewSoapResponse(payload interface{}, relatesTo string) (SoapResponse, error) {
// Sanity check
if len(relatesTo) == 0 {
return SoapResponse{}, errors.New("relatesTo is invalid")
}
// Useful constants
// Some of these are string urls to be assigned to pointers - they need to have a type and cannot be const literals
var (
urlNSS = mdm.EnrollNSS
urlNSA = mdm.EnrollNSA
urlXSI = mdm.EnrollXSI
urlXSD = mdm.EnrollXSD
urlXSU = mdm.EnrollXSU
urlDiag = mdm.ActionNsDiag
urlDiscovery = mdm.ActionNsDiscovery
urlPolicy = mdm.ActionNsPolicy
urlEnroll = mdm.ActionNsEnroll
urlSecExt = mdm.EnrollSecExt
MUValue = "1"
timestampID = "_0"
secWindowStartTimeMin = -5
secWindowEndTimeMin = 5
)
// string pointers - they need to be pointers to not be marshalled into the XML when nil
var (
headerXsu *string
action string
activityID *mdm_types.ActivityId
security *mdm_types.WsSecurity
)
// Build the response body
var body mdm_types.BodyResponse
// Set the message specific fields based on the message type
switch msg := payload.(type) {
case *mdm_types.DiscoverResponse:
action = urlDiscovery
uuid := uuid.New().String()
activityID = &mdm_types.ActivityId{
Content: uuid,
CorrelationId: uuid,
XmlNS: urlDiag,
}
body.DiscoverResponse = msg
case *mdm_types.GetPoliciesResponse:
action = urlPolicy
headerXsu = &urlXSU
body.Xsi = &urlXSI
body.Xsd = &urlXSD
body.GetPoliciesResponse = msg
case *mdm_types.RequestSecurityTokenResponseCollection:
action = urlEnroll
headerXsu = &urlXSU
security = &mdm_types.WsSecurity{
MustUnderstand: MUValue,
XmlNS: urlSecExt,
Timestamp: mdm_types.Timestamp{
ID: timestampID,
Created: getUtcTime(secWindowStartTimeMin), // minutes ago
Expires: getUtcTime(secWindowEndTimeMin), // minutes from now
},
}
body.RequestSecurityTokenResponseCollection = msg
// Setting the target action
case *mdm_types.SoapFault:
if msg.OriginalMessageType == mdm_types.MDEDiscovery {
action = urlDiscovery
} else if msg.OriginalMessageType == mdm_types.MDEPolicy {
action = urlPolicy
} else if msg.OriginalMessageType == mdm_types.MDEEnrollment {
action = urlEnroll
} else {
action = urlDiag
}
uuid := uuid.New().String()
activityID = &mdm_types.ActivityId{
Content: uuid,
CorrelationId: uuid,
XmlNS: urlDiag,
}
body.SoapFault = msg
default:
return SoapResponse{}, errors.New("mdm response message not supported")
}
// Return the SoapRequest type with the appropriate fields set
return SoapResponse{
XmlNSS: urlNSS,
XmlNSA: urlNSA,
XmlNSU: headerXsu,
Header: mdm_types.ResponseHeader{
Action: mdm_types.Action{
Content: action,
MustUnderstand: MUValue,
},
RelatesTo: relatesTo,
ActivityId: activityID,
Security: security,
},
Body: body,
}, nil
}
// MDM SOAP request decoder
func (req *SoapRequest) DecodeBody(ctx context.Context, r io.Reader) error {
// Reading the request bytes
reqBytes, err := io.ReadAll(r)
if err != nil {
return ctxerr.Wrap(ctx, err, "reading soap mdm request")
}
// Unmarshal the XML data from the request into the SoapRequest struct
err = xml.Unmarshal(reqBytes, &req)
if err != nil {
return ctxerr.Wrap(ctx, err, "unmarshalling soap mdm request")
}
return nil
}
// mdmMicrosoftDiscoveryEndpoint is the response struct for the GetDiscovery endpoint
func mdmMicrosoftDiscoveryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*SoapRequest)
// Checking first if Discovery message is valid and returning error if this is not the case
if err := req.IsValidDiscoveryMsg(); err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err)
return getSoapResponseFault(req.getMessageID(), soapFault), nil
}
// Getting the DiscoveryResponse message
discoveryMessage, err := svc.GetMDMMicrosoftDiscoveryResponse(ctx)
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err)
return getSoapResponseFault(req.getMessageID(), soapFault), nil
}
// Embedding the DiscoveryResponse message inside of a SoapResponse
response, err := NewSoapResponse(discoveryMessage, req.getMessageID())
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err)
return getSoapResponseFault(req.getMessageID(), soapFault), nil
}
return response, nil
}
// GetMDMMicrosoftDiscoveryResponse returns a valid DiscoveryResponse message
func (svc *Service) GetMDMMicrosoftDiscoveryResponse(ctx context.Context) (*fleet.DiscoverResponse, error) {
// skipauth: This endpoint does not use authentication
svc.authz.SkipAuthorization(ctx)
// Getting the app config
appCfg, err := svc.ds.AppConfig(ctx)
if err != nil {
return nil, ctxerr.Wrap(ctx, err)
}
// Getting the DiscoveryResponse message content ready
urlDiscoveryEndpoint, err := mdm.ResolveMicrosoftMDMDiscovery(appCfg.ServerSettings.ServerURL)
if err != nil {
return nil, ctxerr.Wrap(ctx, err)
}
urlPolicyEndpoint, err := mdm.ResolveMicrosoftMDMPolicy(appCfg.ServerSettings.ServerURL)
if err != nil {
return nil, ctxerr.Wrap(ctx, err)
}
urlEnrollEndpoint, err := mdm.ResolveMicrosoftMDMEnroll(appCfg.ServerSettings.ServerURL)
if err != nil {
return nil, ctxerr.Wrap(ctx, err)
}
discoveryMsg, err := NewDiscoverResponse(urlDiscoveryEndpoint, urlPolicyEndpoint, urlEnrollEndpoint)
if err != nil {
return nil, ctxerr.Wrap(ctx, err)
}
return &discoveryMsg, nil
}
// GetAuthorizedSoapFault authorize the request so SoapFault message can be returned
func (svc *Service) GetAuthorizedSoapFault(ctx context.Context, eType string, origMsg int, errorMsg error) *fleet.SoapFault {
svc.authz.SkipAuthorization(ctx)
soapFault := NewSoapFault(eType, origMsg, errorMsg)
return &soapFault
}

View File

@ -0,0 +1,195 @@
package service
import (
"encoding/xml"
"errors"
"fmt"
"testing"
mdm_types "github.com/fleetdm/fleet/v4/server/fleet"
mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/stretchr/testify/require"
)
// NewSoapRequest takes a SOAP request in the form of a byte slice and tries to unmarshal it into a SoapRequest struct.
func NewSoapRequest(request []byte) (SoapRequest, error) {
// Sanity check on input
if len(request) == 0 {
return SoapRequest{}, errors.New("soap request is invalid")
}
// Unmarshal the XML data from the request into the SoapRequest struct
var req SoapRequest
err := xml.Unmarshal(request, &req)
if err != nil {
return req, fmt.Errorf("there was a problem unmarshalling soap request: %v", err)
}
// If there was no error, return the SoapRequest and a nil error
return req, nil
}
func TestValidSoapResponse(t *testing.T) {
relatesTo := "urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749"
soapFaultMsg := NewSoapFault(mdm.SoapErrorAuthentication, mdm_types.MDEDiscovery, errors.New("test"))
sres, err := NewSoapResponse(&soapFaultMsg, relatesTo)
require.NoError(t, err)
outXML, err := xml.MarshalIndent(sres, "", " ")
require.NoError(t, err)
require.NotEmpty(t, outXML)
require.Contains(t, string(outXML), fmt.Sprintf("<a:RelatesTo>%s</a:RelatesTo>", relatesTo))
}
func TestInvalidSoapResponse(t *testing.T) {
relatesTo := "urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749"
_, err := NewSoapResponse(relatesTo, relatesTo)
require.Error(t, err)
}
func TestFaultMessageSoapResponse(t *testing.T) {
targetErrorString := "invalid input request"
soapFaultMsg := NewSoapFault(mdm.SoapErrorAuthentication, mdm_types.MDEDiscovery, errors.New(targetErrorString))
sres, err := NewSoapResponse(&soapFaultMsg, "urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749")
require.NoError(t, err)
outXML, err := xml.MarshalIndent(sres, "", " ")
require.NoError(t, err)
require.NotEmpty(t, outXML)
require.Contains(t, string(outXML), fmt.Sprintf("<s:text xml:lang=\"en-us\">%s</s:text>", targetErrorString))
}
// func NewRequestSecurityTokenResponseCollection(provisionedToken string) (mdm_types.RequestSecurityTokenResponseCollection, error) {
func TestRequestSecurityTokenResponseCollectionSoapResponse(t *testing.T) {
provisionedToken := "provisionedToken"
reqSecTokenCollectionMsg, err := NewRequestSecurityTokenResponseCollection(provisionedToken)
require.NoError(t, err)
sres, err := NewSoapResponse(&reqSecTokenCollectionMsg, "urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749")
require.NoError(t, err)
outXML, err := xml.MarshalIndent(sres, "", " ")
require.NoError(t, err)
require.NotEmpty(t, outXML)
require.Contains(t, string(outXML), fmt.Sprintf("base64binary\">%s</BinarySecurityToken>", provisionedToken))
}
func TestGetPoliciesResponseSoapResponse(t *testing.T) {
minKey := "2048"
getPoliciesMsg, err := NewGetPoliciesResponse(minKey, "10", "20")
require.NoError(t, err)
sres, err := NewSoapResponse(&getPoliciesMsg, "urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749")
require.NoError(t, err)
outXML, err := xml.MarshalIndent(sres, "", " ")
require.NoError(t, err)
require.NotEmpty(t, outXML)
require.Contains(t, string(outXML), fmt.Sprintf("<minimalKeyLength>%s</minimalKeyLength>", minKey))
}
func TestValidSoapRequestWithDiscoverMsg(t *testing.T) {
requestBytes := []byte(`
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover</a:Action>
<a:MessageID>urn:uuid:748132ec-a575-4329-b01b-6171a9cf8478</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">https://mdmwindows.com:443/EnrollmentServer/Discovery.svc</a:To>
</s:Header>
<s:Body>
<Discover xmlns="http://schemas.microsoft.com/windows/management/2012/01/enrollment">
<request xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<EmailAddress>demo@mdmwindows.com</EmailAddress>
<RequestVersion>5.0</RequestVersion>
<DeviceType>CIMClient_Windows</DeviceType>
<ApplicationVersion>6.2.9200.2965</ApplicationVersion>
<OSEdition>48</OSEdition>
<AuthPolicies>
<AuthPolicy>OnPremise</AuthPolicy>
<AuthPolicy>Federated</AuthPolicy>
</AuthPolicies>
</request>
</Discover>
</s:Body>
</s:Envelope>
`)
req, err := NewSoapRequest(requestBytes)
require.NoError(t, err)
err = req.IsValidDiscoveryMsg()
require.NoError(t, err)
}
func TestInvalidSoapRequestWithDiscoverMsg(t *testing.T) {
requestBytes := []byte(`
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wst="http://docs.oasis-open.org/ws-sx/ws-trust/200512" xmlns:ac="http://schemas.xmlsoap.org/ws/2006/12/authorization">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep</a:Action>
<a:MessageID>urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">https://mdmwindows.com/EnrollmentServer/Enrollment.svc</a:To>
<wsse:Security s:mustUnderstand="1">
<wsse:BinarySecurityToken ValueType="http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentUserToken" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">aGVsbG93b3JsZA==</wsse:BinarySecurityToken>
</wsse:Security>
</s:Header>
<s:Body>
<wst:RequestSecurityToken>
<wst:TokenType>http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken</wst:TokenType>
<wst:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</wst:RequestType>
<wsse:BinarySecurityToken ValueType="http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">MIICzjCCAboCAQAwSzFJMEcGA1UEAxNAMkI5QjUyQUMtREYzOC00MTYxLTgxNDItRjRCMUUwIURCMjU3QzNBMDg3NzhGNEZCNjFFMjc0OTA2NkMxRjI3ADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKogsEpbKL8fuXpTNAE5RTZim8JO5CCpxj3z+SuWabs/s9Zse6RziKr12R4BXPiYE1zb8god4kXxet8x3ilGqAOoXKkdFTdNkdVa23PEMrIZSX5MuQ7mwGtctayARxmDvsWRF/icxJbqSO+bYIKvuifesOCHW2cJ1K+JSKijTMik1N8NFbLi5fg1J+xImT9dW1z2fLhQ7SNEMLosUPHsbU9WKoDBfnPsLHzmhM2IMw+5dICZRoxHZalh70FefBk0XoT8b6w4TIvc8572TyPvvdwhc5o/dvyR3nAwTmJpjBs1YhJfSdP+EBN1IC2T/i/mLNUuzUSC2OwiHPbZ6MMr/hUCAwEAAaBCMEAGCSqGSIb3DQEJDjEzMDEwLwYKKwYBBAGCN0IBAAQhREIyNTdDM0EwODc3OEY0RkI2MUUyNzQ5MDY2QzFGMjcAMAkGBSsOAwIdBQADggEBACQtxyy74sCQjZglwdh/Ggs6ofMvnWLMq9A9rGZyxAni66XqDUoOg5PzRtSt+Gv5vdLQyjsBYVzo42W2HCXLD2sErXWwh/w0k4H7vcRKgEqv6VYzpZ/YRVaewLYPcqo4g9NoXnbW345OPLwT3wFvVR5v7HnD8LB2wHcnMu0fAQORgafCRWJL1lgw8VZRaGw9BwQXCF/OrBNJP1ivgqtRdbSoH9TD4zivlFFa+8VDz76y2mpfo0NbbD+P0mh4r0FOJan3X9bLswOLFD6oTiyXHgcVSzLN0bQ6aQo0qKp3yFZYc8W4SgGdEl07IqNquKqJ/1fvmWxnXEbl3jXwb1efhbM=</wsse:BinarySecurityToken>
<ac:AdditionalContext xmlns="http://schemas.xmlsoap.org/ws/2006/12/authorization">
<ac:ContextItem Name="UXInitiated">
<ac:Value>false</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="HWDevID">
<ac:Value>BF2D12A95AE42E47D58465E9A71336CAF33FCCAD3088F140F4D50B371FB2256F</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="Locale">
<ac:Value>en-US</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="TargetedUserLoggedIn">
<ac:Value>true</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="OSEdition">
<ac:Value>48</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="DeviceName">
<ac:Value>DESKTOP-0C89RC0</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="MAC">
<ac:Value>00-0C-29-7B-4E-4C</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="MAC">
<ac:Value>00-0C-29-7B-4E-56</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="DeviceID">
<ac:Value>DB257C3A08778F4FB61E2749066C1F27</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="EnrollmentType">
<ac:Value>Full</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="DeviceType">
<ac:Value>CIMClient_Windows</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="OSVersion">
<ac:Value>10.0.19045.2965</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="ApplicationVersion">
<ac:Value>10.0.19045.2965</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="NotInOobe">
<ac:Value>false</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="RequestVersion">
<ac:Value>5.0</ac:Value>
</ac:ContextItem>
</ac:AdditionalContext>
</wst:RequestSecurityToken>
</s:Body>
</s:Envelope>
`)
req, err := NewSoapRequest(requestBytes)
require.NoError(t, err)
err = req.IsValidDiscoveryMsg()
require.Error(t, err)
}

View File

@ -13,11 +13,11 @@ type Middleware struct {
svc fleet.Service
}
func NewAppleMiddleware(svc fleet.Service) *Middleware {
func NewMDMConfigMiddleware(svc fleet.Service) *Middleware {
return &Middleware{svc: svc}
}
func (m *Middleware) Verify() endpoint.Middleware {
func (m *Middleware) VerifyAppleMDM() endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req interface{}) (interface{}, error) {
if err := m.svc.VerifyMDMAppleConfigured(ctx); err != nil {
@ -28,3 +28,15 @@ func (m *Middleware) Verify() endpoint.Middleware {
}
}
}
func (m *Middleware) VerifyMicrosoftMDM() endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req interface{}) (interface{}, error) {
if err := m.svc.VerifyMDMMicrosoftConfigured(ctx); err != nil {
return nil, err
}
return next(ctx, req)
}
}
}

View File

@ -14,7 +14,8 @@ type mockService struct {
mock.Mock
fleet.Service
mdmConfigured atomic.Bool
mdmConfigured atomic.Bool
msMdmConfigured atomic.Bool
}
func (m *mockService) VerifyMDMAppleConfigured(ctx context.Context) error {
@ -24,10 +25,17 @@ func (m *mockService) VerifyMDMAppleConfigured(ctx context.Context) error {
return nil
}
func (m *mockService) VerifyMDMMicrosoftConfigured(ctx context.Context) error {
if !m.msMdmConfigured.Load() {
return fleet.ErrMDMNotConfigured
}
return nil
}
func TestMDMConfigured(t *testing.T) {
svc := mockService{}
svc.mdmConfigured.Store(true)
mw := NewAppleMiddleware(&svc)
mw := NewMDMConfigMiddleware(&svc)
nextCalled := false
next := func(ctx context.Context, req interface{}) (interface{}, error) {
@ -35,7 +43,7 @@ func TestMDMConfigured(t *testing.T) {
return struct{}{}, nil
}
f := mw.Verify()(next)
f := mw.VerifyAppleMDM()(next)
_, err := f(context.Background(), struct{}{})
require.NoError(t, err)
require.True(t, nextCalled)
@ -44,7 +52,7 @@ func TestMDMConfigured(t *testing.T) {
func TestMDMNotConfigured(t *testing.T) {
svc := mockService{}
svc.mdmConfigured.Store(false)
mw := NewAppleMiddleware(&svc)
mw := NewMDMConfigMiddleware(&svc)
nextCalled := false
next := func(ctx context.Context, req interface{}) (interface{}, error) {
@ -52,7 +60,41 @@ func TestMDMNotConfigured(t *testing.T) {
return struct{}{}, nil
}
f := mw.Verify()(next)
f := mw.VerifyAppleMDM()(next)
_, err := f(context.Background(), struct{}{})
require.ErrorIs(t, err, fleet.ErrMDMNotConfigured)
require.False(t, nextCalled)
}
func TestMicrosoftMDMConfigured(t *testing.T) {
svc := mockService{}
svc.msMdmConfigured.Store(true)
mw := NewMDMConfigMiddleware(&svc)
nextCalled := false
next := func(ctx context.Context, req interface{}) (interface{}, error) {
nextCalled = true
return struct{}{}, nil
}
f := mw.VerifyMicrosoftMDM()(next)
_, err := f(context.Background(), struct{}{})
require.NoError(t, err)
require.True(t, nextCalled)
}
func TestMicrosoftMDMNotConfigured(t *testing.T) {
svc := mockService{}
svc.msMdmConfigured.Store(false)
mw := NewMDMConfigMiddleware(&svc)
nextCalled := false
next := func(ctx context.Context, req interface{}) (interface{}, error) {
nextCalled = true
return struct{}{}, nil
}
f := mw.VerifyMicrosoftMDM()(next)
_, err := f(context.Background(), struct{}{})
require.ErrorIs(t, err, fleet.ErrMDMNotConfigured)
require.False(t, nextCalled)

View File

@ -11,8 +11,9 @@ import (
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/fleet"
windows_mdm "github.com/fleetdm/fleet/v4/server/mdm/windows"
"github.com/go-kit/kit/log/level"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
)
type setOrbitNodeKeyer interface {
@ -205,15 +206,15 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
}
}
// set the host's orbit notifications for Windows MDM
if config.MDM.WindowsEnabledAndConfigured {
if host.IsElegibleForWindowsMDMEnrollment() {
discoURL, err := windows_mdm.ResolveWindowsMDMDiscovery(config.ServerSettings.ServerURL)
// set the host's orbit notifications for Microsoft MDM
if config.MDM.MicrosoftEnabledAndConfigured {
if host.IsElegibleForMicrosoftMDMEnrollment() {
discoURL, err := microsoft_mdm.ResolveMicrosoftMDMDiscovery(config.ServerSettings.ServerURL)
if err != nil {
return fleet.OrbitConfig{Notifications: notifs}, err
}
notifs.WindowsMDMDiscoveryEndpoint = discoURL
notifs.NeedsProgrammaticWindowsMDMEnrollment = true
notifs.MicrosoftMDMDiscoveryEndpoint = discoURL
notifs.NeedsProgrammaticMicrosoftMDMEnrollment = true
}
}

View File

@ -1,5 +1,5 @@
# Windows MDM Server Demo
# Microsoft MDM Server Demo
This project is a working and minimal implementation of the Windows device enrollment and management protocols. It was based on an initial implementation of the MS-MDE enrollment protocols [here](https://github.com/oscartbeaumont/windows_mdm).

View File

@ -16,15 +16,17 @@ import (
// Code forked from https://github.com/oscartbeaumont/windows_mdm
// Global config, populated via Command line flags
var domain string
var deepLinkUserEmail string
var authPolicy string
var profileDir string
var staticDir string
var verbose bool
var (
domain string
deepLinkUserEmail string
authPolicy string
profileDir string
staticDir string
verbose bool
)
func main() {
fmt.Println("Starting Windows MDM Demo Server")
fmt.Println("Starting Microsoft MDM Demo Server")
// Parse CMD flags. This populates the varibles defined above
flag.StringVar(&domain, "domain", "mdmwindows.com", "Your servers primary domain")
@ -63,20 +65,20 @@ func main() {
// Create HTTP request router
r := mux.NewRouter()
//MS-MDE and MS-MDM endpoints
// MS-MDE and MS-MDM endpoints
r.Path("/EnrollmentServer/Discovery.svc").Methods("GET", "POST").HandlerFunc(DiscoveryHandler)
r.Path("/EnrollmentServer/Policy.svc").Methods("POST").HandlerFunc(PolicyHandler)
r.Path("/EnrollmentServer/Enrollment.svc").Methods("POST").HandlerFunc(EnrollHandler)
r.Path("/ManagementServer/MDM.svc").Methods("POST").HandlerFunc(ManageHandler)
//Static root endpoint
// Static root endpoint
r.Path("/").Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
w.Write([]byte(`<center><h1>FleetDM Windows MDM Demo Server<br></h1>.<center>`))
w.Write([]byte(`<center><h1>FleetDM Microsoft MDM Demo Server<br></h1>.<center>`))
w.Write([]byte(`<br><center><img src="https://fleetdm.com/images/press-kit/fleet-logo-dark-rgb.png"></center>`))
})
//Static file serve
// Static file serve
fileServer := http.FileServer(http.Dir(staticDir))
r.PathPrefix("/").Handler(http.StripPrefix("/static", fileServer))
@ -111,7 +113,6 @@ func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, body []byte, err error) {
// global HTTP handler to log input and output https traffic
func globalHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if verbose {
// grabbing Input Header and Body
reqHeader, err := httputil.DumpRequest(r, false)

View File

@ -38,9 +38,9 @@ func main() {
if scanner.Text() == " b == '?' ||" {
scanner.Scan()
if scanner.Text() != " b == '!' || // Windows MDM Certificate Parsing Patch" {
out.Write([]byte(" b == '!' || // Windows MDM Certificate Parsing Patch\n"))
out.Write([]byte(" b == 0 || // Windows MDM Certificate Parsing Patch\n"))
if scanner.Text() != " b == '!' || // Microsoft MDM Certificate Parsing Patch" {
out.Write([]byte(" b == '!' || // Microsoft MDM Certificate Parsing Patch\n"))
out.Write([]byte(" b == 0 || // Microsoft MDM Certificate Parsing Patch\n"))
}
out.Write(scanner.Bytes())