mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
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:
parent
e95e075e77
commit
22bb16bf2e
1
changes/issue-12261-microsoft-mdm-discovery-endpoint
Normal file
1
changes/issue-12261-microsoft-mdm-discovery-endpoint
Normal file
@ -0,0 +1 @@
|
|||||||
|
* Microsoft MDM Enrollment Protocol: Added support for the DiscoveryRequest messages
|
@ -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.
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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() &&
|
||||||
|
343
server/fleet/microsoft_mdm.go
Normal file
343
server/fleet/microsoft_mdm.go
Normal 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:"-"`
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
124
server/mdm/microsoft/microsoft_mdm.go
Normal file
124
server/mdm/microsoft/microsoft_mdm.go
Normal 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)
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
@ -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(`
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
631
server/service/microsoft_mdm.go
Normal file
631
server/service/microsoft_mdm.go
Normal 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
|
||||||
|
}
|
195
server/service/microsoft_mdm_test.go
Normal file
195
server/service/microsoft_mdm_test.go
Normal 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)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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).
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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())
|
||||||
|
Loading…
Reference in New Issue
Block a user