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{}) emptySetupAsst := writeTmpJSON(t, map[string]any{})
// Apply global config with custom setting and macos setup assistant, and enable // Apply global config with custom setting and macos setup assistant, and enable
// Windows MDM. // Microsoft MDM.
name = writeTmpYml(t, fmt.Sprintf(`--- name = writeTmpYml(t, fmt.Sprintf(`---
apiVersion: v1 apiVersion: v1
kind: config kind: config
@ -929,7 +929,7 @@ spec:
MacOSSettings: fleet.MacOSSettings{ MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath}, CustomSettings: []string{mobileConfigPath},
}, },
WindowsEnabledAndConfigured: true, MicrosoftEnabledAndConfigured: true,
}, currentAppConfig.MDM) }, currentAppConfig.MDM)
// start a server to return the bootstrap package // start a server to return the bootstrap package
@ -962,7 +962,7 @@ spec:
MacOSSettings: fleet.MacOSSettings{ MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath}, CustomSettings: []string{mobileConfigPath},
}, },
WindowsEnabledAndConfigured: true, MicrosoftEnabledAndConfigured: true,
}, currentAppConfig.MDM) }, currentAppConfig.MDM)
// Apply team config. // 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 ##### mdm.windows_enabled_and_configured
Enables or disables Windows MDM support. Enables or disables Microsoft MDM support.
- Default value: false - Default value: false
- Config file format: - 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) 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)) outputCmd, err := localUTF16toString(unsafe.Pointer(outputStrBuffer))
if err != nil { if err != nil {
return "", err return "", err

View File

@ -1659,11 +1659,11 @@ func SetTestMDMConfig(t testing.TB, cfg *FleetConfig, cert, key []byte, appleBMT
cfg.MDM.AppleSCEPChallenge = "testchallenge" 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: // MDM feature is visible in the UI and can be enabled. More details here:
// https://github.com/fleetdm/fleet/issues/12257 // 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. // release.
func IsMDMFeatureFlagEnabled() bool { func IsMDMFeatureFlagEnabled() bool {
return os.Getenv("FLEET_DEV_MDM_ENABLED") == "1" return os.Getenv("FLEET_DEV_MDM_ENABLED") == "1"

View File

@ -151,11 +151,11 @@ type MDM struct {
MacOSMigration MacOSMigration `json:"macos_migration"` MacOSMigration MacOSMigration `json:"macos_migration"`
EndUserAuthentication MDMEndUserAuthentication `json:"end_user_authentication"` 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 // There is no other configuration required for Windows other than enabling
// the support, but it is still called "EnabledAndConfigured" for consistency // the support, but it is still called "EnabledAndConfigured" for consistency
// with the similarly named macOS-specific fields. // 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 // 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() h.IsDEPAssignedToFleet()
} }
// IsElegibleForWindowsMDMEnrollment returns true if the host can be enrolled // IsElegibleForMicrosoftMDMEnrollment returns true if the host can be enrolled
// in Fleet's Windows MDM (if Windows MDM was enabled). // in Fleet's Microsoft MDM (if Microsoft MDM was enabled).
func (h *Host) IsElegibleForWindowsMDMEnrollment() bool { func (h *Host) IsElegibleForMicrosoftMDMEnrollment() bool {
return h.FleetPlatform() == "windows" && return h.FleetPlatform() == "windows" &&
h.IsOsqueryEnrolled() && h.IsOsqueryEnrolled() &&
!h.MDMInfo.IsEnrolledInThirdPartyMDM() && !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 // fleetd (orbit) so that it can run commands or more generally react to this
// information. // information.
type OrbitConfigNotifications struct { type OrbitConfigNotifications struct {
RenewEnrollmentProfile bool `json:"renew_enrollment_profile,omitempty"` RenewEnrollmentProfile bool `json:"renew_enrollment_profile,omitempty"`
RotateDiskEncryptionKey bool `json:"rotate_disk_encryption_key,omitempty"` RotateDiskEncryptionKey bool `json:"rotate_disk_encryption_key,omitempty"`
NeedsMDMMigration bool `json:"needs_mdm_migration,omitempty"` NeedsMDMMigration bool `json:"needs_mdm_migration,omitempty"`
NeedsProgrammaticWindowsMDMEnrollment bool `json:"needs_programmatic_windows_mdm_enrollment,omitempty"` NeedsProgrammaticMicrosoftMDMEnrollment bool `json:"needs_programmatic_microsoft_mdm_enrollment,omitempty"`
WindowsMDMDiscoveryEndpoint string `json:"windows_mdm_discovery_endpoint,omitempty"` MicrosoftMDMDiscoveryEndpoint string `json:"microsoft_mdm_discovery_endpoint,omitempty"`
} }
type OrbitConfig struct { type OrbitConfig struct {

View File

@ -696,6 +696,11 @@ type Service interface {
// error can be raised to the user. // error can be raised to the user.
VerifyMDMAppleConfigured(ctx context.Context) error 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 MDMAppleUploadBootstrapPackage(ctx context.Context, name string, pkg io.Reader, teamID uint) error
GetMDMAppleBootstrapPackageBytes(ctx context.Context, token string) (*MDMAppleBootstrapPackage, error) GetMDMAppleBootstrapPackageBytes(ctx context.Context, token string) (*MDMAppleBootstrapPackage, error)
@ -746,4 +751,13 @@ type Service interface {
ResetAutomation(ctx context.Context, teamIDs, policyIDs []uint) error ResetAutomation(ctx context.Context, teamIDs, policyIDs []uint) error
RequestEncryptionKeyRotation(ctx context.Context, hostID 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 // MDMEnabled is true if fleet serve was started with
// FLEET_DEV_MDM_ENABLED=1. // FLEET_DEV_MDM_ENABLED=1.
// //
// Undocumented feature flag for Windows MDM, used to determine if the // 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 // Microsoft MDM feature is visible in the UI and can be enabled. More details
// here: https://github.com/fleetdm/fleet/issues/12257 // 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. // release.
MDMEnabled bool `json:"mdm_enabled,omitempty"` MDMEnabled bool `json:"mdm_enabled,omitempty"`
} }
@ -648,7 +648,7 @@ func (svc *Service) validateMDM(
// Windows validation // Windows validation
if !config.IsMDMFeatureFlagEnabled() { 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") invalid.Append("mdm.windows_enabled_and_configured", "cannot enable Windows MDM without the feature flag explicitly enabled")
return return
} }

View File

@ -37,6 +37,8 @@ import (
"github.com/throttled/throttled/v2" "github.com/throttled/throttled/v2"
"go.elastic.co/apm/module/apmgorilla/v2" "go.elastic.co/apm/module/apmgorilla/v2"
otmiddleware "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" otmiddleware "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
) )
type errorHandler struct { 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 // endpoint that's behind the mdmConfiguredMiddleware, this applies
// both to this set of endpoints and to any public/token-authenticated // both to this set of endpoints and to any public/token-authenticated
// endpoints using `neMDM` below in this file. // endpoints using `neMDM` below in this file.
mdmConfiguredMiddleware := mdmconfigured.NewAppleMiddleware(svc) mdmConfiguredMiddleware := mdmconfigured.NewMDMConfigMiddleware(svc)
mdm := ue.WithCustomMiddleware(mdmConfiguredMiddleware.Verify()) mdmAppleMW := ue.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleMDM())
mdm.POST("/api/_version_/fleet/mdm/apple/enqueue", enqueueMDMAppleCommandEndpoint, enqueueMDMAppleCommandRequest{}) mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/enqueue", enqueueMDMAppleCommandEndpoint, enqueueMDMAppleCommandRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/commandresults", getMDMAppleCommandResultsEndpoint, getMDMAppleCommandResultsRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/commandresults", getMDMAppleCommandResultsEndpoint, getMDMAppleCommandResultsRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/commands", listMDMAppleCommandsEndpoint, listMDMAppleCommandsRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/commands", listMDMAppleCommandsEndpoint, listMDMAppleCommandsRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/filevault/summary", getMdmAppleFileVaultSummaryEndpoint, getMDMAppleFileVaultSummaryRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/filevault/summary", getMdmAppleFileVaultSummaryEndpoint, getMDMAppleFileVaultSummaryRequest{})
mdm.POST("/api/_version_/fleet/mdm/apple/profiles", newMDMAppleConfigProfileEndpoint, newMDMAppleConfigProfileRequest{}) mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/profiles", newMDMAppleConfigProfileEndpoint, newMDMAppleConfigProfileRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesEndpoint, listMDMAppleConfigProfilesRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesEndpoint, listMDMAppleConfigProfilesRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", getMDMAppleConfigProfileEndpoint, getMDMAppleConfigProfileRequest{}) mdmAppleMW.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{}) mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", deleteMDMAppleConfigProfileEndpoint, deleteMDMAppleConfigProfileRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/profiles/summary", getMDMAppleProfilesSummaryEndpoint, getMDMAppleProfilesSummaryRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles/summary", getMDMAppleProfilesSummaryEndpoint, getMDMAppleProfilesSummaryRequest{})
mdm.POST("/api/_version_/fleet/mdm/apple/enrollment_profile", createMDMAppleSetupAssistantEndpoint, createMDMAppleSetupAssistantRequest{}) mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/enrollment_profile", createMDMAppleSetupAssistantEndpoint, createMDMAppleSetupAssistantRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/enrollment_profile", getMDMAppleSetupAssistantEndpoint, getMDMAppleSetupAssistantRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/enrollment_profile", getMDMAppleSetupAssistantEndpoint, getMDMAppleSetupAssistantRequest{})
mdm.DELETE("/api/_version_/fleet/mdm/apple/enrollment_profile", deleteMDMAppleSetupAssistantEndpoint, deleteMDMAppleSetupAssistantRequest{}) mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/enrollment_profile", deleteMDMAppleSetupAssistantEndpoint, deleteMDMAppleSetupAssistantRequest{})
// TODO: are those undocumented endpoints still needed? I think they were only used // TODO: are those undocumented endpoints still needed? I think they were only used
// by 'fleetctl apple-mdm' sub-commands. // by 'fleetctl apple-mdm' sub-commands.
mdm.POST("/api/_version_/fleet/mdm/apple/installers", uploadAppleInstallerEndpoint, uploadAppleInstallerRequest{}) mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/installers", uploadAppleInstallerEndpoint, uploadAppleInstallerRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/installers/{installer_id:[0-9]+}", getAppleInstallerEndpoint, getAppleInstallerDetailsRequest{}) mdmAppleMW.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{}) mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/installers/{installer_id:[0-9]+}", deleteAppleInstallerEndpoint, deleteAppleInstallerDetailsRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/installers", listMDMAppleInstallersEndpoint, listMDMAppleInstallersRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/installers", listMDMAppleInstallersEndpoint, listMDMAppleInstallersRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/devices", listMDMAppleDevicesEndpoint, listMDMAppleDevicesRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/devices", listMDMAppleDevicesEndpoint, listMDMAppleDevicesRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/dep/devices", listMDMAppleDEPDevicesEndpoint, listMDMAppleDEPDevicesRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/dep/devices", listMDMAppleDEPDevicesEndpoint, listMDMAppleDEPDevicesRequest{})
// bootstrap-package routes // bootstrap-package routes
mdm.POST("/api/_version_/fleet/mdm/apple/bootstrap", uploadBootstrapPackageEndpoint, uploadBootstrapPackageRequest{}) mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/bootstrap", uploadBootstrapPackageEndpoint, uploadBootstrapPackageRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/bootstrap/{team_id:[0-9]+}/metadata", bootstrapPackageMetadataEndpoint, bootstrapPackageMetadataRequest{}) mdmAppleMW.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{}) mdmAppleMW.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.GET("/api/_version_/fleet/mdm/apple/bootstrap/summary", getMDMAppleBootstrapPackageSummaryEndpoint, getMDMAppleBootstrapPackageSummaryRequest{})
// host-specific mdm routes // host-specific mdm routes
mdm.PATCH("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/unenroll", mdmAppleCommandRemoveEnrollmentProfileEndpoint, mdmAppleCommandRemoveEnrollmentProfileRequest{}) mdmAppleMW.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{}) mdmAppleMW.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{}) mdmAppleMW.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.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/wipe", deviceWipeEndpoint, deviceWipeRequest{})
mdm.PATCH("/api/_version_/fleet/mdm/apple/settings", updateMDMAppleSettingsEndpoint, updateMDMAppleSettingsRequest{}) mdmAppleMW.PATCH("/api/_version_/fleet/mdm/apple/settings", updateMDMAppleSettingsEndpoint, updateMDMAppleSettingsRequest{})
mdm.PATCH("/api/_version_/fleet/mdm/apple/setup", updateMDMAppleSetupEndpoint, updateMDMAppleSetupRequest{}) mdmAppleMW.PATCH("/api/_version_/fleet/mdm/apple/setup", updateMDMAppleSetupEndpoint, updateMDMAppleSetupRequest{})
mdm.GET("/api/_version_/fleet/mdm/apple", getAppleMDMEndpoint, nil) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple", getAppleMDMEndpoint, nil)
mdm.POST("/api/_version_/fleet/mdm/apple/setup/eula", createMDMAppleEULAEndpoint, createMDMAppleEULARequest{}) mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/setup/eula", createMDMAppleEULAEndpoint, createMDMAppleEULARequest{})
mdm.GET("/api/_version_/fleet/mdm/apple/setup/eula/metadata", getMDMAppleEULAMetadataEndpoint, getMDMAppleEULAMetadataRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/setup/eula/metadata", getMDMAppleEULAMetadataEndpoint, getMDMAppleEULAMetadataRequest{})
mdm.DELETE("/api/_version_/fleet/mdm/apple/setup/eula/{token}", deleteMDMAppleEULAEndpoint, deleteMDMAppleEULARequest{}) mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/setup/eula/{token}", deleteMDMAppleEULAEndpoint, deleteMDMAppleEULARequest{})
mdm.POST("/api/_version_/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileEndpoint, preassignMDMAppleProfileRequest{}) mdmAppleMW.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/match", matchMDMApplePreassignmentEndpoint, matchMDMApplePreassignmentRequest{})
// the following set of mdm endpoints must always be accessible (even // the following set of mdm endpoints must always be accessible (even
// if MDM is not configured) as it bootstraps the setup of MDM // 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{}) ).GET("/api/_version_/fleet/device/{token}/transparency", transparencyURL, transparencyURLRequest{})
// mdm-related endpoints available via device authentication // mdm-related endpoints available via device authentication
demdm := de.WithCustomMiddleware(mdmConfiguredMiddleware.Verify()) demdm := de.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleMDM())
demdm.WithCustomMiddleware( demdm.WithCustomMiddleware(
errorLimiter.Limit("get_device_mdm", desktopQuota), errorLimiter.Limit("get_device_mdm", desktopQuota),
).GET("/api/_version_/fleet/device/{token}/mdm/apple/manual_enrollment_profile", getDeviceMDMManualEnrollProfileEndpoint, getDeviceMDMManualEnrollProfileRequest{}) ).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 // `service.mdmAppleConfigurationRequiredEndpoints` when you add an
// endpoint that's behind the mdmConfiguredMiddleware, this applies // endpoint that's behind the mdmConfiguredMiddleware, this applies
// both to this set of endpoints and to any user authenticated // both to this set of endpoints and to any user authenticated
// endpoints using `mdm.*` above in this file. // endpoints using `mdmAppleMW.*` above in this file.
neMDM := ne.WithCustomMiddleware(mdmConfiguredMiddleware.Verify()) neAppleMDM := ne.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleMDM())
neMDM.GET(apple_mdm.EnrollPath, mdmAppleEnrollEndpoint, mdmAppleEnrollRequest{}) neAppleMDM.GET(apple_mdm.EnrollPath, mdmAppleEnrollEndpoint, mdmAppleEnrollRequest{})
neMDM.GET(apple_mdm.InstallerPath, mdmAppleGetInstallerEndpoint, mdmAppleGetInstallerRequest{}) neAppleMDM.GET(apple_mdm.InstallerPath, mdmAppleGetInstallerEndpoint, mdmAppleGetInstallerRequest{})
neMDM.HEAD(apple_mdm.InstallerPath, mdmAppleHeadInstallerEndpoint, mdmAppleHeadInstallerRequest{}) neAppleMDM.HEAD(apple_mdm.InstallerPath, mdmAppleHeadInstallerEndpoint, mdmAppleHeadInstallerRequest{})
neMDM.GET("/api/_version_/fleet/mdm/apple/bootstrap", downloadBootstrapPackageEndpoint, downloadBootstrapPackageRequest{}) neAppleMDM.GET("/api/_version_/fleet/mdm/apple/bootstrap", downloadBootstrapPackageEndpoint, downloadBootstrapPackageRequest{})
neMDM.GET("/api/_version_/fleet/mdm/apple/setup/eula/{token}", getMDMAppleEULAEndpoint, getMDMAppleEULARequest{}) 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{}) 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), errorLimiter.Limit("ping_orbit", desktopQuota),
).HEAD("/api/fleet/orbit/ping", orbitPingEndpoint, orbitPingRequest{}) ).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{}) 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{}) 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" } "mdm": { "apple_bm_default_team": "xyz" }
}`), http.StatusUnprocessableEntity, &acResp) }`), 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) // (only set in mdm integrations tests)
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "windows_enabled_and_configured": true } "mdm": { "windows_enabled_and_configured": true }

View File

@ -2491,6 +2491,7 @@ func (s *integrationEnterpriseTestSuite) TestAppleMDMNotConfigured() {
if route.deviceAuthenticated { if route.deviceAuthenticated {
path = fmt.Sprintf(route.path, tkn) path = fmt.Sprintf(route.path, tkn)
} }
res := s.Do(route.method, path, nil, expectedErr.StatusCode()) res := s.Do(route.method, path, nil, expectedErr.StatusCode())
errMsg := extractServerErrorText(res.Body) errMsg := extractServerErrorText(res.Body)
assert.Contains(t, errMsg, expectedErr.Error()) assert.Contains(t, errMsg, expectedErr.Error())
@ -2793,13 +2794,13 @@ func (s *integrationEnterpriseTestSuite) TestOrbitConfigNudgeSettings() {
// missing orbit key // missing orbit key
s.DoJSON("POST", "/api/fleet/orbit/config", nil, http.StatusUnauthorized, &resp) 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) h := createOrbitEnrolledHost(t, "darwin", "h", s.ds)
resp = orbitGetConfigResponse{} resp = orbitGetConfigResponse{}
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp) 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.Empty(t, resp.NudgeConfig)
require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment) require.False(t, resp.Notifications.NeedsProgrammaticMicrosoftMDMEnrollment)
require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint) require.Empty(t, resp.Notifications.MicrosoftMDMDiscoveryEndpoint)
// set macos_updates // set macos_updates
s.applyConfig([]byte(` s.applyConfig([]byte(`

View File

@ -6,6 +6,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -15,6 +16,7 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -30,7 +32,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mdm/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/ptr"
"github.com/fleetdm/fleet/v4/server/service/mock" "github.com/fleetdm/fleet/v4/server/service/mock"
"github.com/fleetdm/fleet/v4/server/service/schedule" "github.com/fleetdm/fleet/v4/server/service/schedule"
@ -227,8 +229,8 @@ func (s *integrationMDMTestSuite) TearDownTest() {
"mdm": { "macos_settings": { "enable_disk_encryption": false } } "mdm": { "macos_settings": { "enable_disk_encryption": false } }
}`), http.StatusOK) }`), http.StatusOK)
} }
if appCfg.MDM.WindowsEnabledAndConfigured { if appCfg.MDM.MicrosoftEnabledAndConfigured {
// ensure windows MDM is disabled on exit // ensure microsoft MDM is disabled on exit
s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "windows_enabled_and_configured": false } "mdm": { "windows_enabled_and_configured": false }
}`), http.StatusOK) }`), http.StatusOK)
@ -1730,83 +1732,6 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() {
s.DoJSON("DELETE", deletePath, nil, http.StatusBadRequest, &deleteResp) 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() { func (s *integrationMDMTestSuite) TestAppConfigMDMAppleProfiles() {
t := s.T() t := s.T()
@ -5088,6 +5013,196 @@ func (s *integrationMDMTestSuite) TestMDMMigration() {
checkMigrationResponses(host, token) 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() { func (s *integrationMDMTestSuite) runWorker() {
err := s.worker.ProcessJobs(context.Background()) err := s.worker.ProcessJobs(context.Background())
require.NoError(s.T(), err) require.NoError(s.T(), err)
@ -5095,3 +5210,13 @@ func (s *integrationMDMTestSuite) runWorker() {
require.NoError(s.T(), err) require.NoError(s.T(), err)
require.Empty(s.T(), pending) 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 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 ds.AppConfigFuncInvoked = false
require.False(t, authzCtx.Checked()) 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 svc fleet.Service
} }
func NewAppleMiddleware(svc fleet.Service) *Middleware { func NewMDMConfigMiddleware(svc fleet.Service) *Middleware {
return &Middleware{svc: svc} 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(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req interface{}) (interface{}, error) { return func(ctx context.Context, req interface{}) (interface{}, error) {
if err := m.svc.VerifyMDMAppleConfigured(ctx); err != nil { 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 mock.Mock
fleet.Service fleet.Service
mdmConfigured atomic.Bool mdmConfigured atomic.Bool
msMdmConfigured atomic.Bool
} }
func (m *mockService) VerifyMDMAppleConfigured(ctx context.Context) error { func (m *mockService) VerifyMDMAppleConfigured(ctx context.Context) error {
@ -24,10 +25,17 @@ func (m *mockService) VerifyMDMAppleConfigured(ctx context.Context) error {
return nil return nil
} }
func (m *mockService) VerifyMDMMicrosoftConfigured(ctx context.Context) error {
if !m.msMdmConfigured.Load() {
return fleet.ErrMDMNotConfigured
}
return nil
}
func TestMDMConfigured(t *testing.T) { func TestMDMConfigured(t *testing.T) {
svc := mockService{} svc := mockService{}
svc.mdmConfigured.Store(true) svc.mdmConfigured.Store(true)
mw := NewAppleMiddleware(&svc) mw := NewMDMConfigMiddleware(&svc)
nextCalled := false nextCalled := false
next := func(ctx context.Context, req interface{}) (interface{}, error) { next := func(ctx context.Context, req interface{}) (interface{}, error) {
@ -35,7 +43,7 @@ func TestMDMConfigured(t *testing.T) {
return struct{}{}, nil return struct{}{}, nil
} }
f := mw.Verify()(next) f := mw.VerifyAppleMDM()(next)
_, err := f(context.Background(), struct{}{}) _, err := f(context.Background(), struct{}{})
require.NoError(t, err) require.NoError(t, err)
require.True(t, nextCalled) require.True(t, nextCalled)
@ -44,7 +52,7 @@ func TestMDMConfigured(t *testing.T) {
func TestMDMNotConfigured(t *testing.T) { func TestMDMNotConfigured(t *testing.T) {
svc := mockService{} svc := mockService{}
svc.mdmConfigured.Store(false) svc.mdmConfigured.Store(false)
mw := NewAppleMiddleware(&svc) mw := NewMDMConfigMiddleware(&svc)
nextCalled := false nextCalled := false
next := func(ctx context.Context, req interface{}) (interface{}, error) { next := func(ctx context.Context, req interface{}) (interface{}, error) {
@ -52,7 +60,41 @@ func TestMDMNotConfigured(t *testing.T) {
return struct{}{}, nil 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{}{}) _, err := f(context.Background(), struct{}{})
require.ErrorIs(t, err, fleet.ErrMDMNotConfigured) require.ErrorIs(t, err, fleet.ErrMDMNotConfigured)
require.False(t, nextCalled) require.False(t, nextCalled)

View File

@ -11,8 +11,9 @@ import (
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host" hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/fleet"
windows_mdm "github.com/fleetdm/fleet/v4/server/mdm/windows"
"github.com/go-kit/kit/log/level" "github.com/go-kit/kit/log/level"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
) )
type setOrbitNodeKeyer interface { 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 // set the host's orbit notifications for Microsoft MDM
if config.MDM.WindowsEnabledAndConfigured { if config.MDM.MicrosoftEnabledAndConfigured {
if host.IsElegibleForWindowsMDMEnrollment() { if host.IsElegibleForMicrosoftMDMEnrollment() {
discoURL, err := windows_mdm.ResolveWindowsMDMDiscovery(config.ServerSettings.ServerURL) discoURL, err := microsoft_mdm.ResolveMicrosoftMDMDiscovery(config.ServerSettings.ServerURL)
if err != nil { if err != nil {
return fleet.OrbitConfig{Notifications: notifs}, err return fleet.OrbitConfig{Notifications: notifs}, err
} }
notifs.WindowsMDMDiscoveryEndpoint = discoURL notifs.MicrosoftMDMDiscoveryEndpoint = discoURL
notifs.NeedsProgrammaticWindowsMDMEnrollment = true 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). 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 // Code forked from https://github.com/oscartbeaumont/windows_mdm
// Global config, populated via Command line flags // Global config, populated via Command line flags
var domain string var (
var deepLinkUserEmail string domain string
var authPolicy string deepLinkUserEmail string
var profileDir string authPolicy string
var staticDir string profileDir string
var verbose bool staticDir string
verbose bool
)
func main() { 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 // Parse CMD flags. This populates the varibles defined above
flag.StringVar(&domain, "domain", "mdmwindows.com", "Your servers primary domain") flag.StringVar(&domain, "domain", "mdmwindows.com", "Your servers primary domain")
@ -63,20 +65,20 @@ func main() {
// Create HTTP request router // Create HTTP request router
r := mux.NewRouter() 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/Discovery.svc").Methods("GET", "POST").HandlerFunc(DiscoveryHandler)
r.Path("/EnrollmentServer/Policy.svc").Methods("POST").HandlerFunc(PolicyHandler) r.Path("/EnrollmentServer/Policy.svc").Methods("POST").HandlerFunc(PolicyHandler)
r.Path("/EnrollmentServer/Enrollment.svc").Methods("POST").HandlerFunc(EnrollHandler) r.Path("/EnrollmentServer/Enrollment.svc").Methods("POST").HandlerFunc(EnrollHandler)
r.Path("/ManagementServer/MDM.svc").Methods("POST").HandlerFunc(ManageHandler) 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) { r.Path("/").Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=UTF-8") 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>`)) 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)) fileServer := http.FileServer(http.Dir(staticDir))
r.PathPrefix("/").Handler(http.StripPrefix("/static", fileServer)) 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 // global HTTP handler to log input and output https traffic
func globalHandler(h http.Handler) http.Handler { func globalHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if verbose { if verbose {
// grabbing Input Header and Body // grabbing Input Header and Body
reqHeader, err := httputil.DumpRequest(r, false) reqHeader, err := httputil.DumpRequest(r, false)

View File

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